<?php

/*
 * Copyright (C) 2020 Deciso B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

namespace OPNsense\Firewall\FieldTypes;

use OPNsense\Core\Config;
use OPNsense\Firewall\Util;
use OPNsense\Firewall\Group;
use OPNsense\Base\FieldTypes\ArrayField;
use OPNsense\Base\FieldTypes\ContainerField;

/**
 * Class FilterRuleContainerField
 * @package OPNsense\Firewall\FieldTypes
 */
class FilterRuleContainerField extends ContainerField
{
    /**
     * Interface group weight
     */
    private static $ifgroups = null;

    /**
     * map rules
     * @return array
     */
    public function serialize()
    {
        $result = [
            'label' => $this->getAttribute('uuid')
        ];
        $map_manual = ['source_net', 'source_not', 'source_port', 'destination_net', 'destination_not',
            'destination_port', 'enabled', 'description', 'sequence', 'action', 'replyto', 'disablereplyto'];
        // 1-on-1 map (with type conversion if needed)
        foreach ($this->iterateItems() as $key => $node) {
            if (!in_array($key, $map_manual)) {
                if (is_a($node, "OPNsense\\Base\\FieldTypes\\BooleanField")) {
                    $result[$key] = !empty((string)$node);
                } elseif (is_a($node, "OPNsense\\Base\\FieldTypes\\ProtocolField")) {
                    if ((string)$node != 'any') {
                        $result[$key] = (string)$node;
                    }
                } else {
                    $result[$key] = (string)$node;
                }
            }
        }
        // source / destination mapping
        if (!empty((string)$this->source_net)) {
            $result['from'] = (string)$this->source_net;
            if (!empty((string)$this->source_not)) {
                $result['from_not'] = true;
            }
            if (!empty((string)$this->source_port)) {
                $result['from_port'] = (string)$this->source_port;
            }
        }
        if (!empty((string)$this->destination_net)) {
            $result['to'] = (string)$this->destination_net;
            if (!empty((string)$this->destination_not)) {
                $result['to_not'] = true;
            }
            if (!empty((string)$this->destination_port)) {
                $result['to_port'] = (string)$this->destination_port;
            }
        }
        if (!$this->overload->isEmpty()) {
            $alias = Util::aliasUuidToName($this->overload->getValue());
            if ($alias !== null) {
                $result['overload'] = $alias;
            } else {
                // fall back to default virusprod table
                unset($result['overload']);
            }
        }
        // field mappings and differences
        $result['disabled'] = empty((string)$this->enabled);
        $result['descr'] = (string)$this->description;
        $result['type'] = (string)$this->action;
        $result['reply-to'] = (string)$this->replyto;
        if (!$this->disablereplyto->isEmpty()) {
            /* XXX: registerFilterRule() merges disablereplyto, which requires the item to only exist when set  */
            $result['disablereplyto'] = true;
        }
        /* XXX this is an approximation of the complex situation and will be removed eventually */
        if (count($this->interface->getValues()) != 1 || !$this->interfacenot->isEmpty()) {
            $result['floating'] = true;
        }

        return $result;
    }

    /**
     * rule priority is treated equally to the legacy rules, first "floating" then groups and single interface
     * rules are handled last
     * @return int priority in the ruleset, sequence should determine sort order.
     */
    public function getPriority()
    {
        $configObj = Config::getInstance()->object();
        $interfaces = $this->interface->getValues();
        $has_interface = false;

        foreach ($interfaces as $interface) {
            if (isset($configObj?->interfaces?->$interface)) {
                $has_interface = true;
                break;
            }
        }

        // Invalid rules (not applied by PF)
        if (!empty($interfaces) && !$has_interface) {
            return 600000;
        }

        /* XXX this is an approximation of the complex situation and will be removed eventually */
        if (count($interfaces) != 1 || !$this->interfacenot->isEmpty()) {
            // floating (multiple interfaces involved)
            return 200000;
        }

        // there can be only one
        $interface = $interfaces[0];

        if ($configObj?->interfaces?->$interface?->type == 'group') {
            if (static::$ifgroups === null) {
                static::$ifgroups = [];
                foreach ((new Group())->ifgroupentry->iterateItems() as $node) {
                    if (!$node->sequence->isEmpty()) {
                        static::$ifgroups[$node->ifname->getValue()] = $node->sequence->asInt();
                    }
                }
            }

            if (!isset(static::$ifgroups[$interface])) {
                static::$ifgroups[$interface] = 0;
            }

            // group type
            return 300000 + static::$ifgroups[$interface];
        }

        // default
        return 400000;
    }
}

/**
 * Class FilterRuleField
 * @package OPNsense\Firewall\FieldTypes
 */
class FilterRuleField extends ArrayField
{
    protected static $internalStaticChildren = [];

    /**
     * @inheritDoc
     */
    public function newContainerField($ref, $tagname)
    {
        $container_node = new FilterRuleContainerField($ref, $tagname);
        $parentmodel = $this->getParentModel();
        $container_node->setParentModel($parentmodel);
        return $container_node;
    }

    protected function actionPostLoadingEvent()
    {
        foreach ($this->internalChildnodes as $node) {
            /**
             * Evaluation order consists of a priority group and a sequence within the set,
             * prefixed with 0 as these precede legacy rules
             **/
            $node->sort_order = sprintf("%d.0%06d", $node->getPriority(), (string)$node->sequence);
            $node->prio_group = (string)$node->getPriority();
        }
        return parent::actionPostLoadingEvent();
    }
}
