<?php

namespace Sabre\VObject;

/**
 * VCALENDAR/VCARD reader
 *
 * This class reads the vobject file, and returns a full element tree.
 *
 * TODO: this class currently completely works 'statically'. This is pointless,
 * and defeats OOP principals. Needs refactoring in a future version.
 *
 * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
 * @author Evert Pot (http://evertpot.com/)
 * @license http://sabre.io/license/ Modified BSD License
 */
class Reader {

    /**
     * If this option is passed to the reader, it will be less strict about the
     * validity of the lines.
     *
     * Currently using this option just means, that it will accept underscores
     * in property names.
     */
    const OPTION_FORGIVING = 1;

    /**
     * If this option is turned on, any lines we cannot parse will be ignored
     * by the reader.
     */
    const OPTION_IGNORE_INVALID_LINES = 2;

    /**
     * Parses the file and returns the top component
     *
     * The options argument is a bitfield. Pass any of the OPTIONS constant to
     * alter the parsers' behaviour.
     *
     * @param string $data
     * @param int $options
     * @return Node
     */
    static function read($data, $options = 0) {

        // Normalizing newlines
        $data = str_replace(array("\r","\n\n"), array("\n","\n"), $data);

        $lines = explode("\n", $data);

        // Unfolding lines
        $lines2 = array();
        foreach($lines as $line) {

            // Skipping empty lines
            if (!$line) continue;

            if ($line[0]===" " || $line[0]==="\t") {
                $lines2[count($lines2)-1].=substr($line,1);
            } else {
                $lines2[] = $line;
            }

        }

        unset($lines);

        reset($lines2);

        return self::readLine($lines2, $options);

    }

    /**
     * Reads and parses a single line.
     *
     * This method receives the full array of lines. The array pointer is used
     * to traverse.
     *
     * This method returns null if an invalid line was encountered, and the
     * IGNORE_INVALID_LINES option was turned on.
     *
     * @param array $lines
     * @param int $options See the OPTIONS constants.
     * @return Node
     */
    static private function readLine(&$lines, $options = 0) {

        $line = current($lines);
        $lineNr = key($lines);
        next($lines);

        // Components
        if (strtoupper(substr($line,0,6)) === "BEGIN:") {

            $componentName = strtoupper(substr($line,6));
            $obj = Component::create($componentName);

            $nextLine = current($lines);

            while(strtoupper(substr($nextLine,0,4))!=="END:") {

                $parsedLine = self::readLine($lines, $options);
                $nextLine = current($lines);

                if (is_null($parsedLine)) {
                    continue;
                }
                $obj->add($parsedLine);

                if ($nextLine===false)
                    throw new ParseException('Invalid VObject. Document ended prematurely.');

            }

            // Checking component name of the 'END:' line.
            if (substr($nextLine,4)!==$obj->name) {
                throw new ParseException('Invalid VObject, expected: "END:' . $obj->name . '" got: "' . $nextLine . '"');
            }
            next($lines);

            return $obj;

        }

        // Properties
        //$result = preg_match('/(?P<name>[A-Z0-9-]+)(?:;(?P<parameters>^(?<!:):))(.*)$/',$line,$matches);

        if ($options & self::OPTION_FORGIVING) {
            $token = '[A-Z0-9-\._]+';
        } else {
            $token = '[A-Z0-9-\.]+';
        }
        $parameters = "(?:;(?P<parameters>([^:^\"]|\"([^\"]*)\")*))?";
        $regex = "/^(?P<name>$token)$parameters:(?P<value>.*)$/i";

        $result = preg_match($regex,$line,$matches);

        if (!$result) {
            if ($options & self::OPTION_IGNORE_INVALID_LINES) {
                return null;
            } else {
                throw new ParseException('Invalid VObject, line ' . ($lineNr+1) . ' did not follow the icalendar/vcard format');
            }
        }

        $propertyName = strtoupper($matches['name']);
        $propertyValue = preg_replace_callback('#(\\\\(\\\\|N|n))#',function($matches) {
            if ($matches[2]==='n' || $matches[2]==='N') {
                return "\n";
            } else {
                return $matches[2];
            }
        }, $matches['value']);

        $obj = Property::create($propertyName, $propertyValue);

        if ($matches['parameters']) {

            foreach(self::readParameters($matches['parameters']) as $param) {
                $obj->add($param);
            }

        }

        return $obj;


    }

    /**
     * Reads a parameter list from a property
     *
     * This method returns an array of Parameter
     *
     * @param string $parameters
     * @return array
     */
    static private function readParameters($parameters) {

        $token = '[A-Z0-9-]+';

        $paramValue = '(?P<paramValue>[^\"^;]*|"[^"]*")';

        $regex = "/(?<=^|;)(?P<paramName>$token)(=$paramValue(?=$|;))?/i";
        preg_match_all($regex, $parameters, $matches,  PREG_SET_ORDER);

        $params = array();
        foreach($matches as $match) {

            if (!isset($match['paramValue'])) {

                $value = null;

            } else {

                $value = $match['paramValue'];

                if (isset($value[0]) && $value[0]==='"') {
                    // Stripping quotes, if needed
                    $value = substr($value,1,strlen($value)-2);
                }

                $value = preg_replace_callback('#(\\\\(\\\\|N|n|;|,))#',function($matches) {
                    if ($matches[2]==='n' || $matches[2]==='N') {
                        return "\n";
                    } else {
                        return $matches[2];
                    }
                }, $value);

            }

            $params[] = new Parameter($match['paramName'], $value);

        }

        return $params;

    }


}
