<?php

namespace App\Http\Controllers;

use App\Models\Texture;
use App\Models\User;
use Blessing\Filter;
use Blessing\Rejection;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Intervention\Image\Facades\Image;
use League\CommonMark\GithubFlavoredMarkdownConverter;

class SkinlibController extends Controller
{
    public function __construct()
    {
        $this->middleware(function (Request $request, $next) {
            /** @var User */
            $user = $request->user();
            /** @var Texture */
            $texture = $request->route('texture');

            if ($texture->uploader != $user->uid && !$user->isAdmin()) {
                return json(trans('skinlib.no-permission'), 1)
                    ->setStatusCode(403);
            }

            return $next($request);
        })->only(['rename', 'privacy', 'type', 'delete']);

        $this->middleware(function (Request $request, $next) {
            /** @var User */
            $user = $request->user();
            /** @var Texture */
            $texture = $request->route('texture');

            if (!$texture->public) {
                if (!Auth::check() || ($user->uid != $texture->uploader && !$user->isAdmin())) {
                    $statusCode = (int) option('status_code_for_private');
                    if ($statusCode === 404) {
                        abort($statusCode, trans('skinlib.show.deleted'));
                    } else {
                        abort(403, trans('skinlib.show.private'));
                    }
                }
            }

            return $next($request);
        })->only(['show', 'info']);
    }

    public function library(Request $request)
    {
        $user = Auth::user();

        // Available filters: skin, steve, alex, cape
        $type = $request->input('filter', 'skin');
        $uploader = $request->input('uploader');
        $keyword = $request->input('keyword');
        $sort = $request->input('sort', 'time');
        $sortBy = $sort == 'time' ? 'upload_at' : $sort;

        return Texture::orderBy($sortBy, 'desc')
            ->when(
                $type === 'skin',
                fn (Builder $query) => $query->whereIn('type', ['steve', 'alex']),
                fn (Builder $query) => $query->where('type', $type),
            )
            ->when($keyword, fn (Builder $query, $keyword) => $query->like('name', $keyword))
            ->when($uploader, fn (Builder $query, $uploader) => $query->where('uploader', $uploader))
            ->when($user, function (Builder $query, User $user) {
                if (!$user->isAdmin()) {
                    // use closure-style `where` clause to lift up SQL priority
                    return $query->where(function (Builder $query) use ($user) {
                        $query
                            ->where('public', true)
                            ->orWhere('uploader', $user->uid);
                    });
                }
            }, function (Builder $query) {
                // show public textures only to anonymous visitors
                return $query->where('public', true);
            })
            ->join('users', 'uid', 'uploader')
            ->select(['tid', 'name', 'type', 'uploader', 'public', 'likes', 'nickname'])
            ->paginate(20);
    }

    public function show(Filter $filter, Texture $texture)
    {
        /** @var User */
        $user = Auth::user();
        /** @var FilesystemAdapter */
        $disk = Storage::disk('textures');

        if ($disk->missing($texture->hash)) {
            if (option('auto_del_invalid_texture')) {
                $texture->delete();
            }
            abort(404, trans('skinlib.show.deleted'));
        }

        $badges = [];
        $uploader = $texture->owner;
        if ($uploader) {
            if ($uploader->isAdmin()) {
                $badges[] = ['text' => 'STAFF', 'color' => 'primary'];
            }

            $badges = $filter->apply('user_badges', $badges, [$uploader]);
        }

        $grid = [
            'layout' => [
                ['md-8', 'md-4'],
            ],
            'widgets' => [
                [
                    ['shared.previewer'],
                    ['skinlib.widgets.show.side'],
                ],
            ],
        ];
        $grid = $filter->apply('grid:skinlib.show', $grid);

        return view('skinlib.show')
            ->with('texture', $texture)
            ->with('grid', $grid)
            ->with('extra', [
                'download' => (bool) option('allow_downloading_texture'),
                'currentUid' => $user ? $user->uid : 0,
                'admin' => $user && $user->isAdmin(),
                'inCloset' => $user && $user->closet()->where('tid', $texture->tid)->count() > 0,
                'uploaderExists' => (bool) $uploader,
                'nickname' => optional($uploader)->nickname ?? trans('general.unexistent-user'),
                'report' => intval(option('reporter_score_modification', 0)),
                'badges' => $badges,
            ]);
    }

    public function info(Texture $texture)
    {
        return $texture;
    }

    public function upload(Filter $filter)
    {
        $grid = [
            'layout' => [
                ['md-6', 'md-6'],
            ],
            'widgets' => [
                [
                    ['skinlib.widgets.upload.input'],
                    ['shared.previewer'],
                ],
            ],
        ];
        $grid = $filter->apply('grid:skinlib.upload', $grid);

        $converter = new GithubFlavoredMarkdownConverter();

        return view('skinlib.upload')
            ->with('grid', $grid)
            ->with('extra', [
                'rule' => ($regexp = option('texture_name_regexp'))
                    ? trans('skinlib.upload.name-rule-regexp', compact('regexp'))
                    : trans('skinlib.upload.name-rule'),
                'privacyNotice' => trans(
                    'skinlib.upload.private-score-notice',
                    ['score' => option('private_score_per_storage')]
                ),
                'score' => (int) auth()->user()->score,
                'scorePublic' => (int) option('score_per_storage'),
                'scorePrivate' => (int) option('private_score_per_storage'),
                'closetItemCost' => (int) option('score_per_closet_item'),
                'award' => (int) option('score_award_per_texture'),
                'contentPolicy' => $converter->convertToHtml(option_localized('content_policy'))->getContent(),
            ]);
    }

    public function handleUpload(
        Request $request,
        Filter $filter,
        Dispatcher $dispatcher,
    ) {
        $file = $request->file('file');
        if ($file && !$file->isValid()) {
            Log::error($file->getErrorMessage());
        }

        $data = $request->validate([
            'name' => [
                'required',
                option('texture_name_regexp') ? 'regex:'.option('texture_name_regexp') : 'string',
            ],
            'file' => 'required|mimes:png|max:'.option('max_upload_file_size'),
            'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
            'public' => 'required|boolean',
        ]);

        /** @var UploadedFile */
        $file = $filter->apply('uploaded_texture_file', $file);

        $name = $data['name'];
        $name = $filter->apply('uploaded_texture_name', $name, [$file]);

        $can = $filter->apply('can_upload_texture', true, [$file, $name]);
        if ($can instanceof Rejection) {
            return json($can->getReason(), 1);
        }

        $type = $data['type'];
        $size = getimagesize($file);

        $maxWidth = option('max_texture_width', 8192);
        if ($size[0] > $maxWidth) {
            $message = trans('skinlib.upload.too-wide', [
                'width' => $size[0],
                'maxWidth' => $maxWidth,
            ]);

            return json($message, 1);
        }

        if ($size[0] % 64 != 0 || $size[1] % 32 != 0) {
            $message = trans('skinlib.upload.invalid-size', [
                'type' => $type === 'cape' ? trans('general.cape') : trans('general.skin'),
                'width' => $size[0],
                'height' => $size[1],
            ]);

            return json($message, 1);
        }

        $ratio = $size[0] / $size[1];
        if ($type == 'steve' || $type == 'alex') {
            if ($ratio != 2 && $ratio != 1 || $type === 'alex' && $ratio === 2) {
                $message = trans('skinlib.upload.invalid-size', [
                    'type' => trans('general.skin'),
                    'width' => $size[0],
                    'height' => $size[1],
                ]);

                return json($message, 1);
            }
        } elseif ($type == 'cape') {
            if ($ratio != 2) {
                $message = trans('skinlib.upload.invalid-size', [
                    'type' => trans('general.cape'),
                    'width' => $size[0],
                    'height' => $size[1],
                ]);

                return json($message, 1);
            }
        }

        $image = Image::make($file);
        $imagick = $image->getCore();
        $imagick->setOption('png:compression-filter', '0');
        $imagick->setOption('png:compression-level', '9');
        $imagick->setOption('png:compression-strategy', '0');
        $imagick->setOption('png:exclude-chunk', 'all');
        $imagick->stripImage();
        $sanitized = $image->encode('png')->getEncoded();

        $hash = hash('sha256', $image->encoded);
        $hash = $filter->apply('uploaded_texture_hash', $hash, [$image]);

        /** @var User */
        $user = Auth::user();

        $duplicated = Texture::where('hash', $hash)
            ->where(
                fn (Builder $query) => $query->where('public', true)->orWhere('uploader', $user->uid)
            )
            ->first();
        if ($duplicated) {
            // if the texture already uploaded was set to private,
            // then allow to re-upload it.
            return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
        }

        $fileSize = ceil(strlen($sanitized) / 1024);
        $isPublic = is_string($data['public'])
            ? $data['public'] === '1'
            : $data['public'];
        $cost = $fileSize * (
            $isPublic
            ? option('score_per_storage')
            : option('private_score_per_storage')
        );
        $cost += option('score_per_closet_item');
        $cost -= option('score_award_per_texture', 0);
        if ($user->score < $cost) {
            return json(trans('skinlib.upload.lack-score'), 1);
        }

        $dispatcher->dispatch('texture.uploading', [$image, $name, $hash]);

        $texture = new Texture();
        $texture->name = $name;
        $texture->type = $type;
        $texture->hash = $hash;
        $texture->size = $fileSize;
        $texture->public = $isPublic;
        $texture->uploader = $user->uid;
        $texture->likes = 1;
        $texture->save();

        /** @var FilesystemAdapter */
        $disk = Storage::disk('textures');
        if ($disk->missing($hash)) {
            $disk->put($hash, $sanitized);
        }

        $user->score -= $cost;
        $user->closet()->attach($texture->tid, ['item_name' => $name]);
        $user->save();

        $dispatcher->dispatch('texture.uploaded', [$texture, $image]);

        return json(trans('skinlib.upload.success', ['name' => $name]), 0, [
            'tid' => $texture->tid,
        ]);
    }

    public function delete(Texture $texture, Dispatcher $dispatcher, Filter $filter)
    {
        $can = $filter->apply('can_delete_texture', true, [$texture]);
        if ($can instanceof Rejection) {
            return json($can->getReason(), 1);
        }

        $dispatcher->dispatch('texture.deleting', [$texture]);

        // check if file occupied
        if (Texture::where('hash', $texture->hash)->count() === 1) {
            Storage::disk('textures')->delete($texture->hash);
        }

        $texture->delete();

        $dispatcher->dispatch('texture.deleted', [$texture]);

        return json(trans('skinlib.delete.success'), 0);
    }

    public function privacy(Texture $texture, Dispatcher $dispatcher, Filter $filter)
    {
        $can = $filter->apply('can_update_texture_privacy', true, [$texture]);
        if ($can instanceof Rejection) {
            return json($can->getReason(), 1);
        }

        $uploader = $texture->owner;
        $score_diff = $texture->size
            * (option('private_score_per_storage') - option('score_per_storage'))
            * ($texture->public ? -1 : 1);
        if ($texture->public && option('take_back_scores_after_deletion', true)) {
            $score_diff -= option('score_award_per_texture', 0);
        }
        if ($uploader->score + $score_diff < 0) {
            return json(trans('skinlib.upload.lack-score'), 1);
        }

        if (!$texture->public) {
            $duplicated = Texture::where('hash', $texture->hash)
                ->where('public', true)
                ->first();
            if ($duplicated) {
                return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
            }
        }

        $dispatcher->dispatch('texture.privacy.updating', [$texture]);

        $uploader->score += $score_diff;
        $uploader->save();

        $texture->public = !$texture->public;
        $texture->save();

        $dispatcher->dispatch('texture.privacy.updated', [$texture]);

        $message = trans('skinlib.privacy.success', [
            'privacy' => (
                $texture->public
                    ? trans('general.public')
                    : trans('general.private')),
        ]);

        return json($message, 0);
    }

    public function rename(
        Request $request,
        Dispatcher $dispatcher,
        Filter $filter,
        Texture $texture,
    ) {
        $data = $request->validate(['name' => [
            'required',
            option('texture_name_regexp')
                ? 'regex:'.option('texture_name_regexp')
                : 'string',
        ]]);
        $name = $data['name'];

        $can = $filter->apply('can_update_texture_name', true, [$texture, $name]);
        if ($can instanceof Rejection) {
            return json($can->getReason(), 1);
        }

        $dispatcher->dispatch('texture.name.updating', [$texture, $name]);

        $old = $texture->replicate();
        $texture->name = $name;
        $texture->save();

        $dispatcher->dispatch('texture.name.updated', [$texture, $old]);

        return json(trans('skinlib.rename.success', ['name' => $name]), 0);
    }

    public function type(
        Request $request,
        Dispatcher $dispatcher,
        Filter $filter,
        Texture $texture,
    ) {
        $data = $request->validate([
            'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
        ]);
        $type = $data['type'];

        $can = $filter->apply('can_update_texture_type', true, [$texture, $type]);
        if ($can instanceof Rejection) {
            return json($can->getReason(), 1);
        }

        $dispatcher->dispatch('texture.type.updating', [$texture, $type]);

        $old = $texture->replicate();
        $texture->type = $type;
        $texture->save();

        $dispatcher->dispatch('texture.type.updated', [$texture, $old]);

        return json(trans('skinlib.model.success', ['model' => $type]), 0);
    }
}
