<?php
/**
 * @package    pixelcms_articles
 *
 * @author     christophf <your@email.com>
 * @copyright  A copyright
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 * @link       http://your.url.com
 */

use Joomla\Registry\Registry;

defined('_JEXEC') or die;

require_once 'BaseModel.php';

class PropertyObject {
    public $name;
    public $show;
    public $value;
    public $unit = null;

    public function __construct(SimpleXMLElement $element, ?string $unit = null)
    {
        $type = (string) $element['data_type'];
        $this->name = (string) $element['name'];

        if ($type === 'enum') {
            $this->show = (string) $element['plain'];
        } else {
            $this->show = (string) $element->__toString();
        }

        if ($unit) {
            $this->unit = $unit;
            $this->show .= " $unit";
        }

        $this->value = (string) $element->__toString();

        if ($type === 'integer' || $type === 'int') {
            $this->value = (int) $this->value;
        } else if ($type === 'float') {
            $this->value = (float) $this->value;
        }
    }
}

class SimpleXMLAdapter extends SimpleXMLElement {
    private static function isValidNode($node): bool
    {
        return isset($node) && $node instanceof SimpleXMLElement;
    }

    private static function isNotEmpty($node): bool
    {
        return !empty((string) $node) && ((string) $node !== '0');
    }

    public function hasProperty($prop): bool
    {
        $propertyNode = $this->xpath($this->getPropSelector($prop));
        if (!isset($propertyNode[0])) return false;
        return static::isValidNode($propertyNode[0]) && static::isNotEmpty($propertyNode[0]);
    }

    public function getProperty($prop): ?SimpleXMLElement
    {
        return ($this->hasProperty($prop)) ? $this->xpath($this->getPropSelector($prop))[0] : null;
    }

    public function getPlainProperty($prop)
    {
        $element = $this->getProperty($prop);
        return ($element) ? $element->plain : null;
    }

    public function getPropertyObject($prop, $unit = null)
    {
        return new PropertyObject($this->getProperty($prop), $unit);
    }

    private function getPropSelector($prop): string
    {
        return is_int($prop) ? "./info[@id='$prop']" : "./info[@name='$prop']";
    }
}

class TraserProps {
    const Arbeitsbreite = 'Arbeitsbreite';
    const Hersteller = 50001;
    const Model = 50002;
    const Zustand = 50012;
    const Status = 50030;
    const OrignalPreis = 50031;
    const tax_type = 50032;
    const Steuersatz = 50033;
    const Währung = 50034;
    const Baujahr = 50004;
    const Beschreibung = 50016;
    const Betriebsstunden = 50005;
    const Leistung = 50003;
    const Geschwindigkeit = 1017;
    const Getriebe = 'Getriebe';
    const Zylinder = 37078;

    const KundenAuftrag = 50020;
    const InterneNr = 50000;

    const Land = 50019;
    const Stadt = 50018;
    const PLZ = 50017;
    const Straße = 50021;

    const TatsaechlicheArbeitsbreite = 37079;
    const Schnittbreite = 24505;
    const BreiteSchneidwerk = 2030;
    const Plattform = 1022;
    const ReifenH = 'Reifen-h';
    const ReifenV = 'Reifen-v';
    const ReifenHCondition = 'Reifen-h %';
    const ReifenVCondition = 'Reifen-v %';
    const Zapfwelle = 1031;
    const Füllmenge = 8102;
    const Reihen = 'Reihen';
    const Fahrgassenschaltung = 'Fahrgassenschaltung';
    const Kabine = 'Kabine';
    const Kabinenfederung = 'Kabinenfederung';
    const Klimaanlage = 'Klimaanlage';
    const Kugelkopfkupplung = 'Kugelkopfkupplung';
    const Zusatzsteuergeräte = 'Zusatzsteuergeräte';
    const ArbeitsscheinwerferVorne = 'Arbeitsscheinwerfer vorne';
    const ArbeitsscheinwerferHinten = 'Arbeitsscheinwerfer hinten';
    const Rodertyp = 'Rodertyp';
    const Trenngeräteart = 'Trenngeräteart';
    const Bunkergröße = 'Bunkergröße';
    const Bereifung = 'Bereifung';
    const Schare = 'Schare';
    const Körper = 'Körper';
    const FrontladerTechnik = 'Frontlader-Technik';
    const Stuergeraete_DW = 'Steuergerät dw';
    const Zusatzsteuergeraete = 'Zusatzsteuergeräte';
    const Bauart = 'Bauart';

    const NON_EQUIPMENT = [
        self::Hersteller,
        self::Model,
        self::Zustand,
        self::Status,
        self::KundenAuftrag,
        self::InterneNr,
        self::OrignalPreis,
        self::tax_type,
        self::Steuersatz,
        self::Währung,
        self::Baujahr,
        self::Beschreibung,
        self::Betriebsstunden,
        self::Leistung,
        self::Geschwindigkeit,
        self::Getriebe,
        self::Zylinder,
        self::Land,
        self::Stadt,
        self::PLZ,
        self::Straße,
        self::Land,
        self::Stadt,
        self::PLZ,
        self::Straße,
        self::ReifenH,
        self::ReifenV,
        self::Kabine,
        self::ReifenHCondition,
        self::ReifenVCondition,
    ];

    const UNITS = [
        self::Arbeitsbreite => 'm',
        self::Schnittbreite => 'm',
        self::BreiteSchneidwerk => 'm',
        self::TatsaechlicheArbeitsbreite => 'm',
    ];

    static $constants;

    static function getConstants() {
        if (static::$constants) return static::$constants;

        $oClass = new ReflectionClass(__CLASS__);
        $constants = array_filter($oClass->getConstants(), function($constant) { return !is_array($constant); });

        return static::$constants = $constants;
    }

    /**
     * @param int|string $prop
     *
     * @return string|null
     *
     */
    static function getMapping($prop): ?string {
        $mapping = array_flip(static::getConstants());

        return $mapping[$prop] ?? null;
    }

    static function getUnit($prop): ?string {
        return self::UNITS[$prop] ?? null;
    }

    /**
     * @param int|string $prop
     * @param bool $strict If *true* we will only return if it was declared
     *
     * @return int|string
     */
    static function getConstByProp($prop, bool $strict = false) {
        return static::getConstants()[static::getMapping($prop)] ?? ($strict ? null : $prop);
    }

    protected $shown = [];
    protected $product;

    public function __construct(SimpleXMLAdapter $product)
    {
        $this->product = $product;
    }

    /**
     * @param string|int|array $prop
     * @param callable $callback
     *
     * @return mixed
     */
    public function show($prop, callable $callback)
    {
        $props = is_array($prop) ? $prop : [$prop];

        foreach ($props as $prop) {
            if ($this->shown[$prop]) return null;

            if ($this->product->hasProperty($prop)) {
                $this->shown[$prop] = true;

                return $callback($prop, $this->product);
            }
        }

        return null;
    }
}

class Category {
    public $id;
    public $name;

    public function  __construct(SimpleXMLAdapter $product) {
        $this->id = (int) $product->attributes()->category_id;
        $this->name = (string) $product->attributes()->category_name;
    }
}

class Condition {
    public $id = 'used';

    const MAPPING = [
        'demonstrated' => 'demonstration',
        'exhibited' => 'showroom_machine',
        null => 'new',
    ];

    public function  __construct(SimpleXMLAdapter $product) {
        $this->id = $this->getIdMapping(trim($product->getProperty(TraserProps::Zustand)));
    }

    private function getIdMapping(?string $condition)
    {
        return self::MAPPING[$condition] ?? $condition;
    }
}

class Vat {
    public $value = null;
    public $separable = true;
    public $inclusive = false;

    public function  __construct(SimpleXMLAdapter $product) {
        $attrs = $product->xpath('./price')[0]->attributes();
        $this->value = (float) $attrs->tax_percentage;
        $this->separable = (bool) (((string) $attrs->taxtype) !== 'tax_cannot_be_stated_separately');

        if ($this->separable) {
            $this->inclusive = (bool) (((string) $attrs->taxtype) === 'inclusive');
        }
    }
}

class Price {
    public $currency = 'EUR';
    public $specialprice = false;
    public $values;

    public function __construct(SimpleXMLAdapter $product, Vat $vat)
    {
        $original = (float) $product->getProperty(TraserProps::OrignalPreis);
        $price = (float) $product->xpath('./price')[0];
        $this->currency = (string) $product->getProperty(TraserProps::Währung);

        // Only products with prices get calculated
        if ($price > 0) {
            if ($vat->inclusive) {
                $this->values = (object) [
                    'netto' => $price / (1 + $vat->value),
                    'brutto' => $price,
                ];
            } else {
                $this->values = (object) [
                    'netto' => $price,
                    'brutto' => $price * (1 + $vat->value),
                ];
            }

            if ($original && $original !== $price) {
                $this->values->old = $original;
                $this->specialprice = true;
            }
        }
    }
}

class Workhours {
    public $value;
    public $unit = 'Std.';
    public $show;

    public function  __construct(SimpleXMLAdapter $product) {
        $this->value = (float) $product->getProperty(TraserProps::Betriebsstunden);
        $this->show = number_format($this->value, 0, '', '.') . " $this->unit";
    }
}

class Power {
    public $PS;
    public $kW;

    public function  __construct(SimpleXMLAdapter $product) {
        $this->PS = (float) $product->getProperty(TraserProps::Leistung);
        $this->kW = (float) round($this->PS / 1.36);
    }
}

class Workwidth {
    public $value;
    public $unit = 'm';
    public $show;

    public function  __construct(SimpleXMLElement $product, $prop) {
        $property = $product->getPropertyObject($prop);
        $this->name = $property->name;
        $this->value = (float) $property->value;

        if ($this->value < 1.0) {
            $this->unit = 'cm';
            $this->value = $this->value * 100;
        }
        $this->show = "$this->value $this->unit";
    }
}

class Manufacturer {
    public $id;
    public $value;

    public function  __construct(SimpleXMLAdapter $product) {
        $manufacturer = $product->getProperty(TraserProps::Hersteller);
        $this->id = hexdec(substr(md5($manufacturer), 1, 3));
        $this->value = (string) $manufacturer;
    }
}

class Wheels {
    public $type;
    public $quality;

    public function __construct(SimpleXMLAdapter $product, string $prop = TraserProps::ReifenV)
    {
        $this->type = (string) $product->getProperty($prop);
        if ($product->hasProperty("$prop %")) {
            $this->quality = (int) $product->getProperty("$prop %");
        }
    }
}

class PhoneNumber {
    public $country_code;
    public $area_code;
    public $call_number;

    public function  __construct(string $number) {
        preg_match('/^(\d{4})(?:\s\(0\))?\s0?(15[0-9]{2}|1[567][0-9]|\d{3,5})\s?([\d\s-]+?)$/', $number, $matches);
        [, $country_code, $area_code, $call_number] = $matches;

        $this->country_code = '+' . number_format($country_code, 0, '', ' ');
        $this->area_code = $area_code;
        $this->call_number = $call_number;
    }
}

class Contact {
    public $description;
    public $email;
    public $phone;
    public $mobile;
    public $fax;
    public $image = null;

    private $first_name;
    private $last_name;


    public function  __construct(SimpleXMLAdapter $contact) {
        $this->first_name = (string) $contact->xpath('./first_name')[0];
        $this->last_name = (string) $contact->xpath('./last_name')[0];
        $this->description = "$this->first_name $this->last_name";
        $this->email = (string) $contact->xpath('./email')[0];
        if ($images = $contact->xpath('./image')) {
            $this->image = (object) [
                'url' => $this->getImageUrl($images[0]),
            ];
        }

        $phone = trim((string) $contact->xpath('./phone')[0]);
        /**
         * Export Format changed in December 2022
         *
         * We check if the old field is there and use the new field as fallback.
         * Because not sure if the format was changed for all customers of MPO.
         */
        if ($contact->xpath('./mobile_phone')) {
            $mobile = trim((string) $contact->xpath('./mobile_phone')[0]);
        } else {
            $mobile = trim((string) $contact->xpath('./mobil')[0]);
        }
        $fax = trim((string) $contact->xpath('./fax')[0]);

        $this->phone = (!empty($phone)) ? new PhoneNumber($phone) : null;
        $this->mobile = (!empty($mobile)) ? new PhoneNumber($mobile) : null;
        $this->fax = (!empty($fax)) ? new PhoneNumber($fax) : null;
    }

    private function getImageUrl(string $url)
    {
        if (strpos($url, '/maschinendaten') !== false) {
            return $url;
        }

        return "/maschinendaten/images/$url";
    }
}

class Dealer {
    public $id;
    public $name;
    public $city;
    public $zip;

    public function  __construct(SimpleXMLAdapter $product) {
        $this->city = (string) $product->getProperty(TraserProps::Stadt);
        $this->zip = (string) $product->getProperty(TraserProps::PLZ);

        $this->id = hash('md5', implode('_', [$this->city, $this->zip]));
    }
}

class Product {
    public $id;
    public $dealer_internal_id;
    public $status;
    public $condition;
    public $manufacturer;
    public $model;
    public $price;
    public $vat;
    public $free_text;
    public $equipment = [];
    public $additional = [];
    public $dealer;

    public function  __construct(SimpleXMLAdapter $product, SimpleXMLAdapter $dealer, $params = []) {
        $props = new TraserProps($product);

        $this->id = (string) $product->attributes()->id;
        $this->dealer_internal_id = (string) $product->getProperty(TraserProps::InterneNr);
        $this->status = (int) $product->getProperty(TraserProps::Status);
        $this->manufacturer = new Manufacturer($product);
        $this->condition = new Condition($product);
        $this->category = new Category($product);
        $this->vat = new Vat($product);
        $this->price = new Price($product, $this->vat);
        if ($product->hasProperty(TraserProps::Betriebsstunden)) {
            $this->workhours = new Workhours($product);
        }
        if ($product->hasProperty(TraserProps::Leistung)) {
            $this->power = new Power($product);
        }
        if ($product->hasProperty(TraserProps::Stadt)) {
            $this->dealer = new Dealer($product);
        }

        $props->show(TraserProps::Baujahr, function ($prop, $product) {
            $this->year = (int) $product->getProperty($prop);
        });

        $props->show(TraserProps::Model, function ($prop, $product) {
            $this->model = (string) $product->getProperty($prop);
        });

        $props->show(TraserProps::Beschreibung, function ($prop, $product) {
            $this->free_text = trim((string) $product->getProperty($prop));
        });
        $props->show([TraserProps::Arbeitsbreite, TraserProps::Schnittbreite], function ($prop, $product) {
            $this->work_width = new Workwidth($product, $prop);
        });
        $props->show(TraserProps::Geschwindigkeit, function ($prop, $product) {
            $this->max_speed = $product->getPropertyObject($prop);
        });
        $props->show(TraserProps::Zylinder, function ($prop, $product) {
            $this->cylinders = $product->getPropertyObject($prop);
        });
        $props->show(TraserProps::Getriebe, function ($prop, $product) {
            $this->transmission = $product->getPropertyObject($prop);
        });
        $props->show(TraserProps::ReifenV, function ($prop, $product) {
            $this->front_wheels = new Wheels($product, $prop);
        });
        $props->show(TraserProps::ReifenH, function ($prop, $product) {
            $this->back_wheels = new Wheels($product, $prop);
        });

        $this->images = array_map(function (string $image) {
            return (object) [
                'href' => "/maschinendaten/images/$image",
            ];
        }, $product->xpath('./images/image'));

        $this->contact_persons = $this->getContacts($product, $dealer);

        foreach ($product->xpath('./info') as $info) {
            $id = (int) $info['id'];
            $name = (string) $info['name'];

            $isEquipment = !(in_array($id, TraserProps::NON_EQUIPMENT) || in_array($name, TraserProps::NON_EQUIPMENT));

            if ($isEquipment) {
                if (strval($info['data_type']) === 'boolean') {
                    $this->equipment[] = (strval($info) === 'Ja') ? (string) $info['name'] : null;
                } else {
                    $mapped = TraserProps::getConstByProp($name, true) ?? TraserProps::getConstByProp($id);
                    $this->additional[] = $props->show($mapped, function ($prop, $product) {
                        return $product->getPropertyObject($prop, TraserProps::getUnit($prop));
                    });
                }
            }
        }

        $this->equipment = array_values(array_filter($this->equipment));
        $this->additional = array_values(array_filter($this->additional));
    }

    private function getContacts(SimpleXMLAdapter $product, SimpleXMLAdapter $dealer)
    {
        return array_map(function (string $contact_id) use ($dealer) {
            $contact = $dealer->xpath("./contacts/contact[@id='$contact_id']")[0];
            return $contact ? new Contact($contact) : null;
        }, $product->xpath('./contact_ids/contact_id'));
    }
}

class Pixelcms_articlesModelMpo_products  extends BaseModel
{
    protected $module = 'traktorpool';
    protected $api_key;
    protected $articleId;
    protected $customer_id;
    protected $contact_persons;
    protected $password;
    protected $username;
    protected $language;
    protected $url;
    protected $ads;
    protected $params;
    protected $allParams;
    protected $menuId;
    protected $filters = [];

    protected $defaults = [
        'print_btn' => 1,
        'price_style' => 0,
        'speed', 750,
        'disable_title_image' => 0,
    ];

    protected $hidden = [
        'tracktorpool_apikey',
        'tracktorpool_customer_id',
        'tracktorpool_username',
        'tracktorpool_password',
        'traktorpool_condition',
        'traktorpool_status',
    ];

    public function __construct(array $config = array())
    {
        $lang = JFactory::getLanguage();
        $langCode = $lang->getTag();
        $langCodeShort = substr($langCode, 0, 2);

        parent::__construct($config);

        /** @var ExtendedRegistry $params - Settings for current Menu Item */
        $params = $this->getState('params');

        $this->menuId = $params->getInt('Itemid');
        $menu = JFactory::getApplication()->getMenu()->getItem($this->menuId);

        $this->params = [];
        $this->allParams = [];
        foreach ($menu->query as $key => $item) {
            if (!in_array($key, $this->hidden)) {
                $this->params[$key] = $item;
            }
            $this->allParams[$key] = $item;
        }

        $this->customer_id = $params->get('tracktorpool_customer_id');

        //$this->print_btn = $jinput->getInt('print_btn');
        $this->articleId = $params->get('articleId', null);

        $this->language = $langCode;

        $this->ads = self::getAds();
    }

    private function getAds()
    {
        $ads = self::checkCache();

        foreach ($ads as $ad) {
            $query = http_build_query(array_merge($this->params, [
                'articleId' => $ad->id,
                'Itemid' => $this->menuId,
            ]));
            $ad->link = JRoute::_("index.php?$query");
        }

        usort($ads, function($b, $a) {
            if ($a->id == $b->id) {
                return 0;
            }
            return ($a->id < $b->id) ? -1 : 1;
        });

        return $ads;
    }

    public function getRelatedAds()
    {
        $ad = $this->getData();
        $relatedAds =  array_values(array_filter($this->ads, function ($relatedAd) use ($ad) {
            return !!($ad->category->id == $relatedAd->category->id && $ad->id != $relatedAd->id);
        }));

        self::fisherYatesShuffle($relatedAds, $ad->id);

        return $relatedAds;
    }

    private static function fisherYatesShuffle(&$items, $seed)
    {
        @mt_srand($seed);
        $items = array_values($items);
        for ($i = count($items) - 1; $i > 0; $i--)
        {
            $j = @mt_rand(0, $i);
            $tmp = $items[$i];
            $items[$i] = $items[$j];
            $items[$j] = $tmp;
        }
    }

    public function getParams()
    {
        return $this->params;
    }

    /**
     *
     * @return mixed
     *
     * @since version
     * @throws Exception
     */
    private function fetchAds()
    {
        // $xmlBody = $this->cURL("http://services.mascus.com/api/getexport.aspx?exportid=$this->customer_id");
        $feed = file_get_contents(JPATH_BASE . '/maschinendaten/traser.xml');
        $feed = preg_replace("/<([^>\s]*)\s*(?:xmlns=*[\"'][^\"']*[\"'])[^>]*>/i", "<$1>", $feed);
        $xml = simplexml_load_string($feed, 'SimpleXMLAdapter', LIBXML_NOCDATA);

        $dealers = $xml->xpath("/exchange/dealers/dealer");
        $products = [];
        foreach($dealers as $dealer) {
            $products[] = array_map(function($product) use ($dealer) {
                return new Product($product, $dealer, $this->getParams());
            }, $dealer->xpath('./ads/ad'));
        }

        return array_reduce($products, 'array_merge', []);
    }

    private function checkCache($force = false)
    {
        $cacheKey = "mpo";
        $ads = $this->cache->get($cacheKey, $this->module);

        if (empty($ads) || $force) {
            $ads = $this->fetchAds();
            $this->cache->store( serialize($ads), $cacheKey, $this->module);
        } else {
            $ads = unserialize($ads);
        }

        return $ads;
    }

    public function revalidateCache()
    {
        $this->ads = $this->checkCache(true);
    }

    /**
     * @param       $url
     * @param array $parameters
     *
     * @return mixed
     *
     * @since version
     * @throws Exception
     */
    protected function cURL($url, $parameters = [])
    {
        $options = [
            CURLOPT_URL => "$url",
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER         => true,
            CURLOPT_ENCODING => "",
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 20,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => "GET",
            CURLOPT_POSTFIELDS => "",
            CURLOPT_HTTPHEADER => [
                "Accept: application/json",
                'Cache-Control: no-cache',
            ],
        ];

        $ch = curl_init();
        curl_setopt_array( $ch, $options );

        try {
            $raw_response  = curl_exec( $ch );

            // validate CURL status
            if(curl_errno($ch))
                throw new Exception(curl_error($ch), 500);

            $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
            $header = substr($raw_response, 0, $header_size);
            $body = substr($raw_response, $header_size);

            $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            if ($status_code != 200) {
                throw new Exception("Response with Status Code [" . $status_code . "].", 500);
            }
        } catch(Exception $ex) {
            if ($ch != null) curl_close($ch);
            throw new Exception($ex);
        }

        if ($ch != null) curl_close($ch);

        return $body;
    }

    public function getData()
    {
        if ($this->articleId) {
            return array_reduce($this->ads, function ($carry, $article) {
                if ($article->id === $this->articleId) {
                    $carry = $article;
                }
                return $carry;
            });
        }
        return $this->ads;
    }
}
