<?php

declare(strict_types=1);

namespace Acms\Services\Field;

use Acms\Services\Facades\Entry;
use Acms\Services\Facades\Image;
use Acms\Services\Facades\LocalStorage;
use Acms\Services\Facades\PublicStorage;
use ACMS_POST_Image;
use Field;
use Field_Validation;

/**
 * image 型カスタムフィールドのアップロード処理。
 *
 * data URL の受け取り・回転指定・normal / large / tiny / square の各サイズ生成・
 * 旧ファイルの削除予約、およびアップロードが無い場合の既存画像の引き継ぎを担う。
 *
 * 移設前は FieldExtractor::extract() 内の 367 行のブランチだった。
 * ロジックはそのまま移し、外側のフィールドループを抜ける continue のみ return へ置き換えている。
 */
class ImageFieldHandler extends UploadFieldHandler
{
    /**
     * @inheritDoc
     *
     * @param array<int, string> $allowFileExtensions image 型では使用しない（file 型と
     *   インターフェースを揃えるために受け取る）
     */
    public function handle(
        string $fd,
        Field_Validation $field,
        Field $post,
        Field $deleteField,
        ProcessingFiles $processing,
        array $allowFileExtensions
    ): void {
        $ARCHIVES_DIR = ARCHIVES_DIR;
        $dataUrl = false;

        // data url
        if (isset($_POST[$fd])) {
            ACMS_POST_Image::base64DataToImage($_POST[$fd], $fd);
            $field->delete($fd);
            $dataUrl = true;
        }

        if (empty($_FILES[$fd])) {
            foreach (
                [
                    'path', 'x', 'y', 'alt', 'fileSize',
                    'largePath', 'largeX', 'largeY', 'largeAlt', 'largeFileSize',
                    'tinyPath', 'tinyX', 'tinyY', 'tinyAlt', 'tinyFileSize',
                    'squarePath', 'squareX', 'squareY', 'squareAlt', 'squareFileSize',
                    'secret'
                ] as $key
            ) {
                $key    = $fd . '@' . $key;
                $deleteField->set($key, []);
                $field->deleteField($fd . '@' . $key);
            }
            return;
        }

        $aryC = $this->normalizer->candidates($_FILES[$fd]);

        foreach (
            [
                'str'   => ['old', 'edit', 'alt', 'filename', 'extension', 'secret'],
                'int'   => [
                    'width', 'height', 'size',
                    'tinyWidth', 'tinyHeight', 'tinySize',
                    'largeWidth', 'largeHeight', 'largeSize',
                    'squareWidth', 'squareHeight', 'squareSize',
                ],
            ] as $_type => $keys
        ) {
            foreach ($keys as $key) {
                foreach ($aryC as $i => $c) {
                    $_field = $fd . '@' . $key;
                    $value  = $post->isExists($_field, $i) ?
                        $post->get($_field, '', $i) : '';
                    $c[$key]    = ('int' == $_type) ? intval($value) : strval($value);
                    $aryC[$i]   = $c;
                }
                $post->delete($fd . '@' . $key);
            }
        }

        $aryData    = [];
        foreach ($aryC as $c) {
            $aryData[]  = [];
        }
        $cnt    = count($aryData);
        for ($i = 0; $i < $cnt; $i++) {
            $c          = $aryC[$i];
            $data       =& $aryData[$i];

            // @rawfilename の展開・null バイト検査・パスの半正規化・.htaccess 拒否
            // （image / file で完全に同一だった前処理）
            $sanitized = $this->normalizer->sanitizePathOptions(
                $c['old'],
                $c['filename'],
                $c['_name']
            );
            if ($sanitized === null) {
                continue;
            }
            $c['old'] = $sanitized['old'];
            $c['filename'] = $sanitized['filename'];

            // アップロード処理中のファイルかどうか
            $isProcessing = $processing->contains($ARCHIVES_DIR . $c['old']);
            //---------------------
            // セキュリティチェック
            // リクエストされた削除ファイル名が怪しい場合に削除と上書きをスキップ
            // このチェックに引っかかった場合にもフィールドの情報は保持する(continueしない)
            // 削除キーがDBに保存されていなかった場合などファイルが消せなくなるため
            // 投稿者以上の権限を持っている場合にもチェックを行わない
            // 暗号化は「フィールド名@パス」をmd5したもの
            // 暗号化文字列の照合にDBは使えない
            // 一回目にフォームを送信するときはDB上にデータがない
            // アップロードが完了したにもかかわらず
            // 他のエラーチェックで引っかかった時は
            // DB上にデータは保存されないため比較できない
            $secretCheck = ( 1
                && !sessionWithSubscription()
                && !empty($c['old'])
                && ( 0
                    or 'delete' == $c['edit']
                    or !empty($c['_tmp_name'])
                )
            ) ? ($c['secret'] == md5($fd . '@' . $c['old'])) : true;

            //----------------------------
            // delete ( 指定削除 continue )
            if (
                1
                && 'delete' == $c['edit']
                && !empty($c['old'])
                && $secretCheck
                && !$isProcessing
                && isExistsRuleModuleConfig()
            ) {
                $this->markFieldFileForRemoval($field, $fd, $c['old'], 'image');
                continue;
            }

            //--------
            // upload
            if (!empty($c['_tmp_name']) and $secretCheck) {
                $tmp_name   = $c['_tmp_name'];
                if (!$dataUrl && !is_uploaded_file($tmp_name)) {
                    continue;
                }
                // getimagesizeが画像ファイルであるかの判定を兼用している
                // @todo security:
                // "GIF89a <SCRIPT>alert('xss');< /SCRIPT>のようなテキストファイルはgetimagesizeを通過する
                // IE6, 7あたりはContent-Typeのほかにファイルの中身も評価してしまう
                // 偽装テキストを読み込んだときに、HTML with JavaScriptとして実行されてしまう可能性がある
                // 参考: http://www.tokumaru.org/d/20071210.html
                if (!($xy = LocalStorage::getImageSize($tmp_name))) {
                    continue;
                }

                //---------------------------
                // delete ( 古いファイルの削除 )
                if (
                    !empty($c['old']) &&
                    !$isProcessing &&
                    isExistsRuleModuleConfig()
                ) {
                    $this->markFieldFileForRemoval($field, $fd, $c['old'], 'image');
                }

                //------------------------------
                // dirname, basename, extension
                if (!empty($c['filename'])) {
                    if (!preg_match('@((?:[^/]*/)*)((?:[^.]*\.)*)(.*)$@', sprintf('%03d', BID) . '/' . $c['filename'], $match)) {
                        throw new \RuntimeException('アップロードファイルのパス解析に失敗しました。');
                    }

                    $extension  = !empty($match[3]) ? $match[3]
                                                    : Image::detectImageExtenstion($xy['mime']);
                    $dirname    = $match[1];
                    $basename   = !empty($match[2]) ? $match[2] . $extension
                                                    : uniqueString() . '.' . $extension;
                } else {
                    $extension = !empty($c['extension'])
                        ? $c['extension'] : Image::detectImageExtenstion($xy['mime']);
                    $dirname    = PublicStorage::archivesDir();
                    $basename   = uniqueString() . '.' . $extension;
                }

                //-------
                // angle
                $angle  = 0;
                if ('rotate' == substr($c['edit'], 0, 6)) {
                    $angle  = intval(substr($c['edit'], 6));
                }

                //--------
                // normal
                $normal     = $dirname . $basename;
                $candidateNormalPath = $ARCHIVES_DIR . $normal;

                // withUniqueFilePath() 内の排他確保（fopen(..., 'x')）は保存先
                // ディレクトリが存在しないと失敗するため、あらかじめ作成しておく
                // （Image::copyImage() 側でもディレクトリ作成は行われるが、それは
                // パス確保より後に実行されるため間に合わない）
                PublicStorage::makeDirectory(dirname($candidateNormalPath));

                // ファイル名が重複している場合はファイル名を変更する。パスの確保と
                // 画像コピーをまとめてアトミックに行う（コピーが失敗したら確保した
                // パスは自動的に削除される）
                $normalPath = PublicStorage::withUniqueFilePath(
                    $candidateNormalPath,
                    '',
                    function (string $normalPath) use ($tmp_name, $c, $angle): string {
                        Image::copyImage($tmp_name, $normalPath, $c['width'], $c['height'], $c['size'], $angle);
                        return $normalPath;
                    }
                );
                $normal = mb_substr($normalPath, strlen($ARCHIVES_DIR));
                $basename = PublicStorage::mbBasename($normalPath);

                if ($xy = PublicStorage::getImageSize($normalPath)) {
                    $data[$fd . '@path']  = $normal;
                    $data[$fd . '@x']     = $xy[0];
                    $data[$fd . '@y']     = $xy[1];
                    $data[$fd . '@alt']   = $c['alt'];
                    $data[$fd . '@fileSize'] = PublicStorage::getFileSize($normalPath);

                    $processing->add($normalPath);
                    Entry::addUploadedFiles($normal); // 新規バージョンとして作成する時にファイルをCOPYするかの判定に利用
                }

                //-------
                // large
                if (!empty($c['largeWidth']) or !empty($c['largeHeight']) or !empty($c['largeSize'])) {
                    $large     = $dirname . 'large-' . $basename;
                    $largePath = $ARCHIVES_DIR . $large;
                    if (!PublicStorage::exists($largePath)) {
                        Image::copyImage($tmp_name, $largePath, $c['largeWidth'], $c['largeHeight'], $c['largeSize'], $angle);
                    }
                    if ($xy = PublicStorage::getImageSize($largePath)) {
                        $data[$fd . '@largePath'] = $large;
                        $data[$fd . '@largeX']    = $xy[0];
                        $data[$fd . '@largeY']    = $xy[1];
                        $data[$fd . '@largeAlt']  = $c['alt'];
                        $data[$fd . '@largeFileSize']  = PublicStorage::getFileSize($largePath);

                        $processing->add($normalPath);
                    }
                }

                //------
                // tiny
                if (!empty($c['tinyWidth']) or !empty($c['tinyHeight']) or !empty($c['tinySize'])) {
                    $tiny     = $dirname . 'tiny-' . $basename;
                    $tinyPath = $ARCHIVES_DIR . $tiny;
                    if (!PublicStorage::exists($tinyPath)) {
                        Image::copyImage($tmp_name, $tinyPath, $c['tinyWidth'], $c['tinyHeight'], $c['tinySize'], $angle);
                    }
                    if ($xy = PublicStorage::getImageSize($tinyPath)) {
                        $data[$fd . '@tinyPath']  = $tiny;
                        $data[$fd . '@tinyX']     = $xy[0];
                        $data[$fd . '@tinyY']     = $xy[1];
                        $data[$fd . '@tinyAlt']   = $c['alt'];
                        $data[$fd . '@tinyFileSize']  = PublicStorage::getFileSize($tinyPath);

                        $processing->add($normalPath);
                    }
                }

                //---------
                // square
                if (!empty($c['squareWidth']) or !empty($c['squareHeight']) or !empty($c['squareSize'])) {
                    $square   = $dirname . 'square-' . $basename;
                    $squarePath = $ARCHIVES_DIR . $square;
                    $squareSize = 0;
                    if (!empty($c['squareWidth'])) {
                        $squareSize = $c['squareWidth'];
                    } elseif (!empty($c['squareHeight'])) {
                        $squareSize = $c['squareHeight'];
                    } elseif (!empty($c['squareSize'])) {
                        $squareSize = $c['squareSize'];
                    }

                    if (!PublicStorage::exists($squarePath)) {
                        Image::copyImage($tmp_name, $squarePath, $squareSize, $squareSize, $squareSize, $angle);
                    }
                    if ($xy = PublicStorage::getImageSize($squarePath)) {
                        $data[$fd . '@squarePath']  = $square;
                        $data[$fd . '@squareX']     = $xy[0];
                        $data[$fd . '@squareY']     = $xy[1];
                        $data[$fd . '@squareAlt']   = $c['alt'];
                        $data[$fd . '@squareFileSize']  = PublicStorage::getFileSize($squarePath);

                        $processing->add($normalPath);
                    }
                }

                //--------
                // secret
                // 正しくファイルがアップロードされた場合のみ新しくキーを発行する
                $data[$fd . '@secret'] = md5($fd . '@' . $normal);

                continue;
            }

            //-----
            // old
            // 非編集アップデートの時
            if (!empty($c['old'])) {
                //--------
                // normal
                $normal = $c['old'];
                $normalPath = $ARCHIVES_DIR . $normal;
                if ($xy = PublicStorage::getImageSize($normalPath)) {
                    $data[$fd . '@path']  = $normal;
                    $data[$fd . '@x']     = $xy[0];
                    $data[$fd . '@y']     = $xy[1];
                    $data[$fd . '@alt']   = $c['alt'];
                    $data[$fd . '@fileSize'] = PublicStorage::getFileSize($normalPath);

                    if (!preg_match('@((?:[^/]*/)*)((?:[^.]*\.)*)(.*)$@', $normal, $match)) {
                        throw new \RuntimeException('既存ファイルのパス解析に失敗しました。');
                    }
                    $extension  = $match[3];
                    $dirname    = $match[1];
                    $basename   = $match[2] . $extension;

                    //-------
                    // large
                    $large     = $dirname . 'large-' . $basename;
                    $largePath = $ARCHIVES_DIR . $large;
                    if ($xy = PublicStorage::getImageSize($largePath)) {
                        $data[$fd . '@largePath'] = $large;
                        $data[$fd . '@largeX']    = $xy[0];
                        $data[$fd . '@largeY']    = $xy[1];
                        $data[$fd . '@largeAlt']  = $c['alt'];
                        $data[$fd . '@largeFileSize']  = PublicStorage::getFileSize($largePath);
                    }

                    //------
                    // tiny
                    $tiny     = $dirname . 'tiny-' . $basename;
                    $tinyPath = $ARCHIVES_DIR . $tiny;
                    if ($xy = PublicStorage::getImageSize($tinyPath)) {
                        $data[$fd . '@tinyPath']  = $tiny;
                        $data[$fd . '@tinyX']     = $xy[0];
                        $data[$fd . '@tinyY']     = $xy[1];
                        $data[$fd . '@tinyAlt']   = $c['alt'];
                        $data[$fd . '@tinyFileSize']  = PublicStorage::getFileSize($tinyPath);
                    }

                    //------
                    // square
                    $square   = $dirname . 'square-' . $basename;
                    $squarePath = $ARCHIVES_DIR . $square;
                    if ($xy = PublicStorage::getImageSize($squarePath)) {
                        $data[$fd . '@squarePath']  = $square;
                        $data[$fd . '@squareX']     = $xy[0];
                        $data[$fd . '@squareY']     = $xy[1];
                        $data[$fd . '@squareAlt']   = $c['alt'];
                        $data[$fd . '@squareFileSize']  = PublicStorage::getFileSize($squarePath);
                    }


                    //--------
                    // secret
                    // これはエラー時にフォームを再表示しなければならない場合に必要
                    $data[$fd . '@secret']  = $c['secret'];
                }
            }
        }

        //------------
        // save field
        $cnt        = count($aryData);
        foreach (
            [
                'path', 'x', 'y', 'alt', 'fileSize',
                'largePath', 'largeX', 'largeY', 'largeAlt', 'largeFileSize',
                'tinyPath', 'tinyX', 'tinyY', 'tinyAlt', 'tinyFileSize',
                'squarePath', 'squareX', 'squareY', 'squareAlt', 'squareFileSize',
                'secret'
            ] as $key
        ) {
            $key    = $fd . '@' . $key;
            $value  = [];
            for ($i = 0; $cnt > $i; $i++) {
                $value[] = !empty($aryData[$i][$key]) ? $aryData[$i][$key] : ''; // @phpstan-ignore-line
            }
            $field->set($key, $value);

            //------------
            // validation
            foreach ($post->listFields() as $_fd) {
                if (preg_match('/^' . $key . ':(?:v#|validator#)(.+)$/', $_fd, $match)) {
                    $method = $match[1];
                    $field->setMethod($key, $method, $post->get($_fd));
                    $post->delete($_fd);
                }
            }
        }
    }
}
