<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

namespace Piwik\Plugin;

use Exception;
use Piwik\Columns\Dimension;
use Piwik\Development;

/**
 * Creates a new segment that can be used for instance within the {@link \Piwik\Columns\Dimension::configureSegment()}
 * method. Make sure to set at least the following values: {@link setName()}, {@link setSegment()},
 * {@link setSqlSegment()}, {@link setType()} and {@link setCategory()}. If you are using a segment in the context of a
 * dimension the type and the SQL segment is usually set for you automatically.
 *
 * Example:
 * ```
 $segment = new \Piwik\Plugin\Segment();
 $segment->setType(\Piwik\Plugin\Segment::TYPE_DIMENSION);
 $segment->setName('General_EntryKeyword');
 $segment->setCategory('General_Visit');
 $segment->setSegment('entryKeyword');
 $segment->setSqlSegment('log_visit.entry_keyword');
 $segment->setAcceptedValues('Any keywords people search for on your website such as "help" or "imprint"');
 ```
 * @api
 * @since 2.5.0
 */
class Segment
{
    /**
     * Segment type 'dimension'. Can be used along with {@link setType()}.
     * @api
     */
    public const TYPE_DIMENSION = 'dimension';

    /**
     * Segment type 'metric'. Can be used along with {@link setType()}.
     * @api
     */
    public const TYPE_METRIC = 'metric';

    private $type;
    private $category;
    private $name;
    private $segment;
    private $sqlSegment;
    private $sqlFilter;
    private $sqlFilterValue;
    private $acceptValues;
    private $permission;
    private $suggestedValuesCallback;
    private $unionOfSegments;
    private $isInternalSegment = false;
    private $suggestedValuesApi = '';
    private $needsMostFrequentValues = true;

    /**
     * If true, this segment will only be visible to a registered user (see API.getSegmentsMetadata).
     *
     * @var bool
     */
    private $requiresRegisteredUser = false;

    /**
     * @ignore
     */
    final public function __construct()
    {
        $this->init();
    }

    /**
     * @var Dimension
     */
    public $dimension;

    /**
     * Here you can initialize this segment and set any default values. It is called directly after the object is
     * created.
     * @api
     */
    protected function init()
    {
    }

    /**
     * Here you should explain which values are accepted/useful for your segment, for example:
     * "1, 2, 3, etc." or "comcast.net, proxad.net, etc.". If the value needs any special encoding you should mention
     * this as well. For example "Any URL including protocol. The URL must be URL encoded."
     *
     * @param string $acceptedValues
     * @api
     */
    public function setAcceptedValues($acceptedValues)
    {
        $this->acceptValues = $acceptedValues;
    }

    /**
     * Set (overwrite) the category this segment belongs to. It should be a translation key such as 'General_Actions'
     * or 'General_Visit'.
     * @param string $category
     * @api
     */
    public function setCategory($category)
    {
        $this->category = $category;
    }

    /**
     * Set (overwrite) the segment display name. This name will be visible in the API and the UI. It should be a
     * translation key such as 'Actions_ColumnEntryPageTitle' or 'Resolution_ColumnResolution'.
     * @param string $name
     * @api
     */
    public function setName($name)
    {
        $this->name = $name;
    }

    /**
     * Set (overwrite) the name of the segment. The name should be lower case first and has to be unique. The segment
     * name defined here needs to be set in the URL to actually apply this segment. Eg if the segment is 'searches'
     * you need to set "&segment=searches>0" in the UI.
     * @param string $segment
     * @api
     */
    public function setSegment($segment)
    {
        $this->segment = $segment;
        $this->check();
    }

    /**
     * Sometimes you want users to set values that differ from the way they are actually stored. For instance if you
     * want to allow to filter by any URL than you might have to resolve this URL to an action id. Or a country name
     * maybe has to be mapped to a 2 letter country code. You can do this by specifying either a callable such as
     * `array('Classname', 'methodName')` or by passing a closure. There will be four values passed to the given closure
     * or callable: `string $valueToMatch`, `string $segment` (see {@link setSegment()}), `string $matchType`
     * (eg SegmentExpression::MATCH_EQUAL or any other match constant of this class) and `$segmentName`.
     *
     * If the closure returns NULL, then Piwik assumes the segment sub-string will not match any visitor.
     *
     * @param callable $sqlFilter
     * @api
     */
    public function setSqlFilter($sqlFilter)
    {
        $this->sqlFilter = $sqlFilter;
    }

    /**
     * Similar to {@link setSqlFilter()} you can map a given segment value to another value. For instance you could map
     * "new" to 0, 'returning' to 1 and any other value to '2'. You can either define a callable or a closure. There
     * will be only one value passed to the closure or callable which contains the value a user has set for this
     * segment. This callback is called shortly before {@link setSqlFilter()}.
     * @param string|array $sqlFilterValue
     * @api
     */
    public function setSqlFilterValue($sqlFilterValue)
    {
        $this->sqlFilterValue = $sqlFilterValue;
    }

    /**
     * Defines to which column in the MySQL database the segment belongs: 'mytablename.mycolumnname'. Eg
     * 'log_visit.idsite'. When a segment is applied the given or filtered value will be compared with this column.
     *
     * @param string $sqlSegment
     * @api
     */
    public function setSqlSegment($sqlSegment)
    {
        $this->sqlSegment = $sqlSegment;
        $this->check();
    }

    /**
     * Set a list of segments that should be used instead of fetching the values from a single column.
     * All set segments will be applied via an OR operator.
     *
     * @param array $segments
     * @api
     */
    public function setUnionOfSegments($segments)
    {
        $this->unionOfSegments = $segments;
        $this->check();
    }

    /**
     * @return array
     * @ignore
     */
    public function getUnionOfSegments()
    {
        return $this->unionOfSegments;
    }

    /**
     * @return string
     * @ignore
     */
    public function getSqlSegment()
    {
        return $this->sqlSegment;
    }

    /**
     * @return string
     * @ignore
     */
    public function getSqlFilterValue()
    {
        return $this->sqlFilterValue;
    }

    /**
     * @return string
     * @ignore
     */
    public function getAcceptValues()
    {
        return $this->acceptValues;
    }

    /**
     * @return string
     * @ignore
     */
    public function getSqlFilter()
    {
        return $this->sqlFilter;
    }

    /**
     * Set (overwrite) the type of this segment which is usually either a 'dimension' or a 'metric'.
     * @param string $type See constants TYPE_*
     * @api
     */
    public function setType($type)
    {
        $this->type = $type;
    }

    /**
     * @return string
     * @ignore
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @return string
     * @ignore
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return string
     * @ignore
     */
    public function getCategoryId()
    {
        return $this->category;
    }

    /**
     * Returns the name of this segment as it should appear in segment expressions.
     *
     * @return string
     */
    public function getSegment()
    {
        return $this->segment;
    }

    /**
     * @return string
     * @ignore
     */
    public function getSuggestedValuesCallback()
    {
        return $this->suggestedValuesCallback;
    }

    /**
     * Set callback which will be executed when user will call for suggested values for segment.
     *
     * @param callable $suggestedValuesCallback
     */
    public function setSuggestedValuesCallback($suggestedValuesCallback)
    {
        $this->suggestedValuesCallback = $suggestedValuesCallback;
    }

    /**
     * @return string
     * @ignore
     */
    public function getSuggestedValuesApi()
    {
        return $this->suggestedValuesApi;
    }

    /**
     * Set callback which will be executed when user will call for suggested values for segment.
     *
     * @param string $suggestedValuesApi
     */
    public function setSuggestedValuesApi($suggestedValuesApi)
    {
        if (!empty($suggestedValuesApi) && is_string($suggestedValuesApi)) {
            if (Development::isEnabled() && strpos($suggestedValuesApi, '.get') === false) {
                throw new Exception('Invalid suggested values API defined, expecting ".get" to be present.');
            }
        } else {
            $suggestedValuesApi = '';
        }
        $this->suggestedValuesApi = $suggestedValuesApi;
    }

    /**
     * @param bool $value
     */
    public function setNeedsMostFrequentValues(bool $value)
    {
        $this->needsMostFrequentValues = $value;
    }

    /**
     * You can restrict the access to this segment by passing a boolean `false`. For instance if you want to make
     * a certain segment only available to users having super user access you could do the following:
     * `$segment->setPermission(Piwik::hasUserSuperUserAccess());`
     * @param bool $permission
     * @api
     */
    public function setPermission($permission)
    {
        $this->permission = $permission;
    }

    /**
     * @return array
     * @ignore
     */
    public function toArray()
    {
        $segment = array(
            'type'                      => $this->type,
            'category'                  => $this->category,
            'name'                      => $this->name,
            'segment'                   => $this->segment,
            'sqlSegment'                => $this->sqlSegment,
            'needsMostFrequentValues'   => $this->needsMostFrequentValues,
        );

        if (!empty($this->unionOfSegments)) {
            $segment['unionOfSegments'] = $this->unionOfSegments;
        }

        if (!empty($this->sqlFilter)) {
            $segment['sqlFilter'] = $this->sqlFilter;
        }

        if (!empty($this->sqlFilterValue)) {
            $segment['sqlFilterValue'] = $this->sqlFilterValue;
        }

        if (!empty($this->acceptValues)) {
            $segment['acceptedValues'] = $this->acceptValues;
        }

        if (isset($this->permission)) {
            $segment['permission'] = $this->permission;
        }

        if (is_callable($this->suggestedValuesCallback)) {
            $segment['suggestedValuesCallback'] = $this->suggestedValuesCallback;
        }

        if (is_string($this->suggestedValuesApi) && !empty($this->suggestedValuesApi)) {
            $segment['suggestedValuesApi'] = $this->suggestedValuesApi;
        }

        return $segment;
    }

    /**
     * Returns true if this segment should only be visible to registered users (see API.getSegmentsMetadata),
     * false if it should always be visible to any user (even the anonymous user).
     *
     * @return boolean
     * @ignore
     */
    public function isRequiresRegisteredUser()
    {
        return $this->requiresRegisteredUser;
    }

    /**
     * Sets whether the segment should only be visible to registered users. If set to false it will be even visible to
     * the anonymous user
     *
     * @param boolean $requiresRegisteredUser
     * @ignore
     */
    public function setRequiresRegisteredUser($requiresRegisteredUser)
    {
        $this->requiresRegisteredUser = $requiresRegisteredUser;
    }

    /**
     * Sets whether the segment is for internal use only and should not be visible in the UI or in API metadata output.
     * These types of segments are, for example, used in unions for other segments, but have no value to users.
     *
     * @param bool $value
     */
    public function setIsInternal($value)
    {
        $this->isInternalSegment = $value;
    }

    /**
     * Gets whether the segment is for internal use only and should not be visible in the UI or in API metadata output.
     * These types of segments are, for example, used in unions for other segments, but have no value to users.
     *
     * @return bool
     */
    public function isInternal()
    {
        return $this->isInternalSegment;
    }

    private function check()
    {
        if ($this->sqlSegment && $this->unionOfSegments) {
            throw new Exception(sprintf('Union of segments and SQL segment is set for segment "%s", use only one of them', $this->name));
        }

        if ($this->segment && $this->unionOfSegments && in_array($this->segment, $this->unionOfSegments, true)) {
            throw new Exception(sprintf('The segment %s contains a union segment to itself', $this->name));
        }
    }
}
