<?php

declare(strict_types=1);

namespace Acms\Services\Field;

use Acms\Services\Facades\Logger as AcmsLogger;
use Acms\Services\Facades\RichEditor;
use Acms\Services\Validator\MimeTypeValidator;
use Field;
use Field_Validation;
use ACMS_Http;

/**
 * POST データからカスタムフィールド（Field_Validation）を組み立てる。
 *
 * image/file カスタムフィールドで旧ファイルを消す操作（削除チェック・差し替え）があった場合、
 * 物理削除は即時には行わず Field のメタ情報 'removeOld' に記録する。
 * 実際の物理削除はバリデーション通過後に呼ばれる FieldRepository::saveField() 内で参照チェック
 * （field / field_rev の両テーブルを検索）の上で行う。
 * よって旧ファイルを残すべきかどうか（新バージョン保存・作業領域のみの保存）は
 * 呼び出し元が判定する必要はなく、参照チェックが自動的に判断する。
 */
class FieldExtractor
{
    /**
     * @var \Field
     */
    protected $Post;

    /**
     * extract()後の削除フィールドを一時保存
     *
     * @var \Field
     */
    protected $deleteField;

    /**
     * @var MimeTypeValidator
     */
    private MimeTypeValidator $mimeTypeValidator;

    /**
     * @var UploadedFilesNormalizer
     */
    private UploadedFilesNormalizer $uploadedFilesNormalizer;

    /**
     * @var FileFieldHandler
     */
    private FileFieldHandler $fileFieldHandler;

    /**
     * @var ImageFieldHandler
     */
    private ImageFieldHandler $imageFieldHandler;

    /**
     * @param (callable(string): void)|null $onMediaCreate アップロードされたメディアの生成通知。
     *   image / file の各ハンドラへそのまま渡す（発火の作法はハンドラ側の基底クラスに閉じている）。
     */
    public function __construct(?callable $onMediaCreate = null)
    {
        $app = \App::getInstance();
        assert($app instanceof \Acms\Application);
        $this->Post =& $app->getPostParameter();
        $this->mimeTypeValidator = new MimeTypeValidator();
        $this->uploadedFilesNormalizer = new UploadedFilesNormalizer();
        $this->fileFieldHandler = new FileFieldHandler(
            $this->uploadedFilesNormalizer,
            $this->mimeTypeValidator,
            $onMediaCreate
        );
        $this->imageFieldHandler = new ImageFieldHandler(
            $this->uploadedFilesNormalizer,
            $this->mimeTypeValidator,
            $onMediaCreate
        );
    }

    /**
     * extract()後の削除フィールドを取得
     *
     * @deprecated extractResult() の戻り値から deleteField() を取得してください。
     *   本メソッドは「extract() の直後に呼ぶ」という暗黙の順序に依存します。
     *
     * @return \Field
     */
    public function getDeleteField()
    {
        return $this->deleteField;
    }

    /**
     * POSTデータからデータの抜き出し
     *
     * 抽出された Field だけを返す互換メソッド。削除フィールドも併せて必要な場合は
     * {@see extractResult()} を使うこと（呼び出し順への依存が無くなる）。
     *
     * @param string $scp
     * @param \ACMS_Validator|null $V
     * @param \Field|null $deleteField
     * @return \Field_Validation
     */
    public function extract($scp = 'field', $V = null, $deleteField = null)
    {
        return $this->extractResult($scp, $V, $deleteField)->field();
    }

    /**
     * POSTデータからデータを抜き出し、抽出結果と削除フィールドをまとめて返す
     *
     * Why not 子フィールドの再帰も extractResult() で行わない: 再帰呼び出しは
     * $this->deleteField を毎回新しい Field で作り直す（移設前からの挙動）。子の処理は
     * image/file の削除フィールド収集より前に走るため実害は無いが、再帰の戻り値を
     * ExtractResult にすると「どの階層の deleteField が最終結果か」が曖昧になる。
     * 再帰は Field_Validation を返す extract() のまま維持し、最終結果の受け渡しだけを
     * 明示的にする。
     *
     * @param string $scp
     * @param \ACMS_Validator|null $V
     * @param \Field|null $deleteField
     * @return ExtractResult
     */
    public function extractResult($scp = 'field', $V = null, $deleteField = null): ExtractResult
    {
        $field = new Field_Validation();
        $this->deleteField = $deleteField;

        $ARCHIVES_DIR = ARCHIVES_DIR;

        if (!$this->deleteField) {
            $this->deleteField = new Field();
        }

        if ($takeover = $this->Post->get($scp . ':takeover')) {
            $takeoverField = acmsUnserialize($takeover);
            if ($takeoverField instanceof Field) {
                $field->overload($takeoverField);
            }
            $this->Post->delete($scp . ':takeover');
        }

        $field->overload($this->Post->dig($scp));
        $this->Post->addChild($scp, $field);

        // 許可ファイル拡張子をまとめておく
        $allow_file_extensions = array_merge(
            configArray('file_extension_document'),
            configArray('file_extension_archive'),
            configArray('file_extension_movie'),
            configArray('file_extension_audio')
        );

        //-------
        // child
        foreach ($field->listFields() as $fd) {
            if (!$this->Post->isExists($fd . ':field')) {
                continue;
            }
            $this->Post->set($fd, $field->getArray($fd));
            $field->delete($fd);
            $childField = $this->extract($fd);
            $field->addChild($fd, $childField);
        }

        // Why not assert なしでハンドラへ渡す: Field::addChild() の引数が `Field &$Field`（参照渡し）の
        // ため、直上の子フィールドループで $field->addChild() を通った時点で静的解析上の型が
        // Field へ広がる。実行時は Field_Validation のままなので、ここで絞り直す。
        assert($field instanceof Field_Validation);

        // アップロード処理中の画像・ファイルを保持する。
        // アップロード処理中のファイルが誤って削除されることを防ぐために利用（image / file 両方が共有する）
        $processingMediaFiles = new ProcessingFiles();
        foreach ($this->Post->listFields() as $metaFd) {
            //-----------
            // converter
            if (
                1
                and preg_match('@^(.+)(?:\:c|\:converter)$@', $metaFd, $match)
                and $field->isExists($match[1])
            ) {
                $fd = $match[1];
                $aryVal = [];
                foreach ($field->getArray($fd) as $val) {
                    $mode = $this->Post->get($metaFd);
                    if (preg_match('/^[rRnNaAsSkKhHcCV]+$/', $mode)) {
                        $aryVal[] = mb_convert_kana($val, $mode, 'UTF-8');
                    } else {
                        AcmsLogger::warning('converterのモードが不正です', [
                            'field' => $fd,
                            'value' => $val,
                            'mode' => $mode,
                        ]);
                        $aryVal[] = '';
                    }
                }
                $field->setField($fd, $aryVal);
                $this->Post->delete($metaFd);
                continue;
            }
            //-----------
            // extension
            if (
                1
                and preg_match('@^(.+):extension$@', $metaFd, $match)
                and $field->isExists($match[1])
            ) {
                $fd         = $match[1];
                $type       = $this->Post->get($fd . ':extension');
                $dataUrl    = false;
                $this->Post->delete($fd . ':extension');

                if ($type === 'media') {
                    foreach ($field->getArray($fd) as $mediaValue) {
                        $field->addField($fd . '@media', $mediaValue);
                    }
                } elseif ($type === 'block-editor') {
                    $field->setMeta($fd, 'type', 'block-editor');
                } elseif ($type === 'paper-editor' || $type === 'rich-editor') {
                    foreach ($field->getArray($fd) as $editorValue) {
                        $field->addField($fd . '@html', RichEditor::render($editorValue));
                        $field->addField($fd . '@title', RichEditor::renderTitle($editorValue));
                    }
                } elseif ($type === 'image' || $type === 'file') {
                    try {
                        $file = ACMS_Http::file($fd);
                        if ($type === 'file') {
                            if ($extensions = $this->Post->getArray($fd . '@extension')) {
                                if (!$this->mimeTypeValidator->validateAllowedByContent($file->getPath(), $extensions)) {
                                    throw new \RuntimeException('EXTENSION_IS_DIFFERENT');
                                }
                            }
                        }
                        $size = $file->getFileSize();
                        if (isset($field->_aryMethod[$fd])) {
                            $arg = $field->_aryMethod[$fd];
                            if (isset($arg['filesize'])) {
                                $maxsize = intval($arg['filesize']);
                                if ($size > ($maxsize * 1024)) {
                                    throw new \RuntimeException(UPLOAD_ERR_FORM_SIZE);
                                }
                            }
                        }
                    } catch (\Exception $e) {
                        if ($e->getMessage() == 'EXTENSION_IS_DIFFERENT') {
                            $field->setMethod($fd, 'extension', false);
                            continue;
                        }
                        if ($e->getMessage() == UPLOAD_ERR_INI_SIZE || $e->getMessage() == UPLOAD_ERR_FORM_SIZE) {
                            $field->setMethod($fd, 'filesize', false);
                            $field->set($fd, 'maxfilesize');
                            continue;
                        }
                    }
                }

                //-------
                // image
                if ('image' == $type) {
                    $this->imageFieldHandler->handle(
                        $fd,
                        $field,
                        $this->Post,
                        $this->deleteField,
                        $processingMediaFiles,
                        $allow_file_extensions
                    );

                //------
                // file
                } elseif ('file' == $type) {
                    $this->fileFieldHandler->handle(
                        $fd,
                        $field,
                        $this->Post,
                        $this->deleteField,
                        $processingMediaFiles,
                        $allow_file_extensions
                    );
                }


                continue;
            }
        }

        //--------
        // search
        foreach ($field->listFields() as $fd) {
            // topic-fix_field_search: Field::getがnullを返さなくなっていたので，無指定時の戻りを擬似定数に変更して対処
            $s = $this->Post->get($fd . ':search', '__NOT_SPECIFIED__');
            if ($s === '__NOT_SPECIFIED__') {
                if (is_int(strpos($fd, '@'))) {
                    $s  = '0';
                } else {
                    $s  = '1';
                }
            }
            $field->setMeta($fd, 'search', $s !== '0');
            $this->Post->deleteField($fd . ':search');
        }

        $field->validate($V);

        return new ExtractResult($field, $this->deleteField);
    }
}
