<?php

namespace OtomaticAi\Modules;

use DOMDocument;
use DOMXPath;
use OtomaticAi\Models\Contracts\Publishable;
use OtomaticAi\Models\Netlinking;
use OtomaticAi\Models\Persona;
use OtomaticAi\Models\Publication;
use OtomaticAi\Models\SingleModeGeneration;
use OtomaticAi\Models\ThumbnailTask;
use OtomaticAi\Services\PublicationGenerationState;
use OtomaticAi\Utils\Language;
use OtomaticAi\Vendors\Illuminate\Support\Arr;
use Throwable;

abstract class Module
{
    /**
     * @var Publishable
     */
    protected Publishable $publication;

    /**
     * @var float
     */
    private float $startTime;

    /**
     * @var float
     */
    private float $endTime;

    /**
     * @var PublicationGenerationState
     */
    protected PublicationGenerationState $generationState;

    /**
     * @var DOMDocument
     */
    protected DOMDocument $html;

    public function __construct(Publishable $publication, PublicationGenerationState $generationState)
    {
        $this->publication = $publication;

        $this->generationState = $generationState;

        $this->html = new DOMDocument('1.0', 'UTF-8');
        @$this->html->loadHTML("<?xml encoding='UTF-8'>" . $this->generationState->getArtifact("html_content", ""), LIBXML_NOWARNING);
    }

    /**
     * @return array
     */
    public function getModules(): array
    {
        if ($this->publication instanceof Publication) {
            return $this->publication->project->modules;
        } elseif ($this->publication instanceof SingleModeGeneration) {
            return $this->publication->modules;
        } elseif ($this->publication instanceof Netlinking) {
            return $this->publication->modules;
        } elseif ($this->publication instanceof ThumbnailTask) {
            return $this->publication->modules;
        }

        return [];
    }

    /**
     * @return array
     */
    static public function getPublicationModules(Publishable $publication): array
    {
        if ($publication instanceof Publication) {
            return $publication->project->modules;
        } elseif ($publication instanceof SingleModeGeneration) {
            return $publication->modules;
        } elseif ($publication instanceof Netlinking) {
            return $publication->modules;
        } elseif ($publication instanceof ThumbnailTask) {
            return $publication->modules;
        }

        return [];
    }

    /**
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    public function getModuleValue(string $key, $default = null)
    {
        return Arr::get($this->getModules(), $key, $default);
    }

    static public function getPublicationModuleValue(Publishable $publication, string $key, $default = null)
    {
        return Arr::get(self::getPublicationModules($publication), $key, $default);
    }

    /**
     * @return Language
     */
    public function getLanguage(): Language
    {
        if ($this->publication instanceof Publication) {
            return $this->publication->project->language;
        } elseif ($this->publication instanceof SingleModeGeneration) {
            return $this->publication->language;
        } elseif ($this->publication instanceof Netlinking) {
            return $this->publication->language;
        }

        return Language::findFromLocale();
    }

    /**
     * @return Persona|null
     */
    public function getPersona(): ?Persona
    {
        if ($this->publication instanceof Publication) {
            return $this->publication->project->persona;
        } elseif ($this->publication instanceof SingleModeGeneration) {
            return $this->publication->persona;
        } elseif ($this->publication instanceof Netlinking) {
            return $this->publication->persona;
        }

        return null;
    }

    /**
     * @return string
     */
    public function getType(): string
    {
        if ($this->publication instanceof Publication) {
            return $this->publication->project->type;
        } elseif ($this->publication instanceof SingleModeGeneration) {
            return $this->publication->type;
        }

        return "bulk";
    }

    /**
     * @return string
     */
    static public function getPublicationType(Publishable $publication): string
    {
        if ($publication instanceof Publication) {
            return $publication->project->type;
        } elseif ($publication instanceof SingleModeGeneration) {
            return $publication->type;
        }

        return "bulk";
    }

    /**
     * @return string
     */
    public function getTitle(): string
    {
        return $this->publication->generation_title;
    }

    /**
     * Add placeholders for the elements in the html and return the placeholders
     * 
     * @param string $element
     * @param string $tag
     * @return array
     */
    protected function createElementPlaceholders(string $element, string $tag = null): array
    {
        $placeholders = [];

        if (empty($tag)) {
            $tag = "oto{$element}";
        }

        $xpath = new DOMXPath($this->html);

        $nodes = $xpath->query("//{$tag}");

        foreach ($nodes as $index => $node) {

            // get the element attributes and add it to the elements array
            $placeholder = [
                "index" => $index,
                'html_content' => $node->ownerDocument->saveHTML($node),
            ];
            foreach ($node->attributes as $attribute) {
                $placeholder[$attribute->name] = $attribute->value;
            }
            $placeholders[] = $placeholder;

            // replace the element with a placeholder
            $placeholderElement = $this->html->createElement("placeholder");
            $placeholderElement->setAttribute("element", $element);
            $placeholderElement->setAttribute("index", $index);
            $node->parentNode->replaceChild($placeholderElement, $node);
        }

        // return the placeholders
        return $placeholders;
    }

    /**
     * Add placeholders for the elements in the html and return the placeholders
     * 
     * @param string $element
     * @param array $tags
     * @return array
     */
    protected function createElementPlaceholdersMultiTags(string $element, array $tags = []): array
    {
        $placeholders = [];

        if (empty($tags)) {
            $tags = ["oto{$element}"];
        }

        $xpath = new DOMXPath($this->html);

        $nodes = $xpath->query(implode(" | ", array_map(function ($tag) {
            return "//{$tag}";
        }, $tags)));

        foreach ($nodes as $index => $node) {

            // get the element attributes and add it to the elements array
            $placeholder = [
                "index" => $index,
            ];
            foreach ($node->attributes as $attribute) {
                $placeholder[$attribute->name] = $attribute->value;
            }
            $placeholders[] = $placeholder;

            // replace the element with a placeholder
            $placeholderElement = $this->html->createElement("placeholder");
            $placeholderElement->setAttribute("element", $element);
            $placeholderElement->setAttribute("index", $index);
            $node->parentNode->replaceChild($placeholderElement, $node);
        }

        // return the placeholders
        return $placeholders;
    }

    /**
     * Count the elements in the html
     * 
     * @param string $element
     * @param string $tag
     * @return int
     */
    protected function countElements(string $element, string $tag = null): int
    {
        if (empty($tag)) {
            $tag = "oto{$element}";
        }

        $xpath = new DOMXPath($this->html);
        $nodes = $xpath->query("//{$tag}");

        return $nodes->length;
    }

    /**
     * Replace the placeholder elements
     *
     * @param string $element
     * @param callable $callback
     * 
     * @return void
     */
    protected function replaceElementPlaceholders(string $element, callable $callback)
    {
        // create xpath
        $xpath = new DOMXPath($this->html);

        $nodes = $xpath->query("//placeholder");
        foreach ($nodes as $index => $node) {

            $key = $node->getAttribute("element");

            if ($key === $element) {

                $attributes = [];
                foreach ($node->attributes as $attribute) {
                    $attributes[$attribute->name] = $attribute->value;
                }

                $replacement = $callback($attributes);
                if (!empty($replacement)) {
                    $node->parentNode->replaceChild($replacement, $node);
                }
            }
        }
    }

    static public function getList(): array
    {
        return [
            ProcessTextModule::class,
            ProcessFaqModule::class,
            ProcessImageModule::class,
            ProcessToolboxModule::class,
            ProcessAmazonModule::class,
            ProcessYoutubeModule::class,
            ProcessCustomFieldsModule::class,
            ProcessWordpressModule::class,
            ProcessFinalizeModule::class,
            ProcessSingleModeFinalizeModule::class,
            ProcessThumbnailFinalizeModule::class,
            ProcessNetlinkingFinalizeModule::class,
        ];
    }

    protected function fail(Throwable $error)
    {
        $this->publication->addLog("Module failed. " . $error->getMessage(), "_default", "error", $error->getTrace());

        $this->publication->status = "failed";
        $this->publication->save();
    }

    protected function startTimer()
    {
        if (empty($this->startTime)) {
            $this->startTime = microtime(true);
        }
    }

    protected function stopTimer()
    {
        if (empty($this->startTime)) {
            return 0;
        }

        if (empty($this->endTime)) {
            $this->endTime = microtime(true);
        }

        // calculate execution time
        return $this->endTime - $this->startTime;
    }

    protected function getExecutionTime()
    {
        if (!empty($this->startTime)) {
            $endTime = microtime(true);
            if (!empty($this->endTime)) {
                $endTime = $this->endTime;
            }

            // calculate execution time
            return $endTime - $this->startTime;
        }

        return 0;
    }
}
