<?php

Yii::setPathOfAlias('vCardParser', Yii::getPathOfAlias('application.components.vCardParser'));

require_once "vCardParser/vCard.php";

class vCardElementTypes
{

    const WORK = 'WORK';
    const HOME = 'HOME';
    const PREF = 'PREF';
    const INTERNET = 'INTERNET';
    const VOICE = 'VOICE';
    const CELL = 'CELL';
    const IPHONE = 'IPHONE';
    const FAX = 'FAX';
    const PERSONAL = 'PERSONAL';
    const BUSINESS = 'BUSINESS';
    const MOBILE = 'MOBILE';

}

class vCardParser extends vCard
{

    const endl = "\r\n";

    private $specialDataArrayKeys = array(
        'IMPP',
        'X-ICQ',
        'X-YAHOO',
        'X-MSN',
        'X-JABBER',
        'X-AIM',
        'URL',
        'X-SOCIALPROFILE',
        'ADR',
        'X-ABDATE',
        'EMAIL',
        'TEL',
        'X-ADDRESSBOOKSERVER-MEMBER'
    );
    protected static $Spec_ElementTypes = array(
        'url' => array(
            vCardElementTypes::WORK,
            vCardElementTypes::HOME,
            vCardElementTypes::PREF,
            vCardElementTypes::PERSONAL,
            vCardElementTypes::BUSINESS,
            vCardElementTypes::MOBILE
        )
    );

    public function readVCards($vCard)
    {
        $this->setParserDefaultOptions($vCard);
        $this->removeLineBreaks();

        if ($this->Mode == self::MODE_MULTIPLE) {
            $this->readMultiCards();
        } else {
            $this->readSingleCards();
        }
    }

    private function setParserDefaultOptions($vCard)
    {
        if ($vCard) {
            $this->RawData = $vCard;
        }

        if (!$this->RawData) {
            throw new CHttpException('404', 'No vCard data given.');
        }
        $this->setVCardParserMode();
    }

    private function setVCardParserMode()
    {
        if ($this->validateVCardData() == 1) {
            $this->Mode = vCard::MODE_SINGLE;
        } else {
            $this->Mode = vCard::MODE_MULTIPLE;
        }
    }

    private function validateVCardData()
    {
        // Counting the begin/end separators. If there aren't any or the count doesn't match, there is a problem with the file.
        // If there is only one, this is a single vCard, if more, multiple vCards are combined.
        $matches = array();
        $vCardBeginCount = preg_match_all('{^BEGIN\:VCARD}miS', $this->RawData, $matches);
        $vCardEndCount = preg_match_all('{^END\:VCARD}miS', $this->RawData, $matches);

        if (($vCardBeginCount != $vCardEndCount) || !$vCardBeginCount) {
            $this->Mode = vCard::MODE_ERROR;
            throw new CHttpException('422', 'Invalid vCard! vCardParser can not process given data.');
        }

        return $vCardBeginCount;
    }

    private function removeLineBreaks()
    {
        // Removing/changing inappropriate newlines, i.e., all CRs or multiple newlines are changed to a single newline
        $this->RawData = str_replace("\r", "\n", $this->RawData);
        $this->RawData = preg_replace('{(\n+)}', "\n", $this->RawData);
    }

    private function readMultiCards()
    {
        // raw text is split at card beginning markers and each
        // fragment is parsed in a separate vCard object.
        $this->RawData = explode('BEGIN:VCARD', $this->RawData);
        $this->RawData = array_filter($this->RawData);
        foreach ($this->RawData as $singlevCardRawData) {
            $singlevCardRawData = 'BEGIN:VCARD' . "\n" . $singlevCardRawData;

            $parser = new vCardParser();
            $parser->readVCards($singlevCardRawData);
            $this->Data[] = $parser;
        }
    }

    private function readSingleCards()
    {
        $this->formattingRawData();
        $lines = explode("\n", $this->RawData);

        foreach ($lines as $line) {
            $this->readLines($line);
        }
    }

    private function formattingRawData()
    {
        // Protect the BASE64 final = sign 
        $this->RawData = preg_replace('{(\n\s.+)=(\n)}', '$1-base64=-$2', $this->RawData);

        $this->RawData = str_replace("=\n", '', $this->RawData);
        $this->RawData = str_replace(array("\n ", "\n\t"), '-wrap-', $this->RawData);

        // Restoring the BASE64 final equals sign (see a few lines above)
        $this->RawData = str_replace("-base64=-\n", "=\n", $this->RawData);
    }

    private function readLines($line)
    {
        if (strpos($line, ':') === false) {
            return null;
        }

        $lineElements = explode(':', $line, 2);
        $key = $lineElements[0];
        $value = '';

        if (isset($lineElements[1])) {
            $value = $lineElements[1];
        }

        if ($key == 'BEGIN' || $key == 'END') {
            return null;
        }

        if ((strpos($key, 'X-SERVICE-TYPE') !== false)) {
            $subType = explode(':', $value, 2);
            $key .= ':' . $subType[0];
            if (isset($subType[1])) {
                $value = $subType[1];
            }
        }

        $formatedKey = $this->removeBackslash($key);
        $formatedLineData = $this->checkForEmbeddedVCards($key, $value);
        if (!$formatedLineData) {
            return null;
        }

        $this->readLineData($formatedKey, $formatedLineData);
    }

    private function checkForEmbeddedVCards($lineKeys, $lineValue)
    {
        if ((strpos($lineKeys, 'AGENT') === 0) && (stripos($lineValue, 'BEGIN:VCARD') !== false)) {
            $formatedLineData = $this->readLineValues($lineValue);
            if (!isset($this->Data[$lineKeys])) {
                $this->Data[$lineKeys] = array();
            }
            $this->Data[$lineKeys][] = $formatedLineData;
            return null;
        } else {
            $formatedLineData = str_replace('-wrap-', '', $lineValue);
        }

        return $this->removeBackslash($formatedLineData);
    }

    private function readLineValues($lineValue)
    {
        $parser = new vCardParser();
        $parser->readVCards(str_replace('-wrap-', "\n", $lineValue));
        return $parser;
    }

    private function readLineData($formatedKey, $value)
    {
        // Here additional parameters are parsed
        $data = $this->getLineDataArray($formatedKey, $value);

        if (strpos($data['key'], 'item') === 0) {
            $data = $this->getItemKeys($data);
        }

        if (count($data['keyParts']) > 1) {
            $parameters = self::ParseParameters($data['key'], array_slice($data['keyParts'], 1));
            $data = $this->getMultipleKeyParts($parameters, $data);
        }
        if (isset($parameters)) {
            $data['value'] = $this->checkForAdditionalParameters($data['key'], $parameters, $data['value']);
        }
        $data['value'] = $this->parseValue($data['value'], $data['key'], $data['type']);

        if (is_array($data['value']) && $data['encoding']) {
            $data['value']['Encoding'] = $data['encoding'];
        }
        $this->addAttributeToDataArray($data['key'], $data['itemIndex'], $data['value']);
    }

    private function getLineDataArray($formatedKey, $value)
    {
        $keyParts = explode(';', $formatedKey);
        return array(
            'keyParts' => $keyParts,
            'key' => $keyParts[0],
            'type' => array(),
            'encoding' => false,
            'itemIndex' => null,
            'value' => $value
        );
    }

    private function getItemKeys($data)
    {
        $tmpKey = explode('.', $data['key'], 2);
        $data['value'] = array($tmpKey[1] => $this->parseValue($data['value'], $tmpKey[1], $data['type'], true));
        $data['key'] = 'item';
        $data['itemIndex'] = (int) str_ireplace('item', '', $tmpKey[0]);

        return $data;
    }

    private function getMultipleKeyParts($parameters, $data)
    {
        foreach ($parameters as $paramKey => $paramValue) {
            if ($paramKey == 'encoding') {
                $data['encoding'] = $paramValue;
            }
            if ($paramKey == 'charset') { // v2.1
                $data['value'] = $this->encodeValues($paramValue, $data['value']);
            }
            if ($paramKey == 'type') {
                $data['type'] = $paramValue;
            }
        }

        return $data;
    }

    private function parseValue($value, $key, $type, $isItem = false)
    {
        $keyLC = strtolower($key);
        if (isset(self::$Spec_StructuredElements[$keyLC])) {
            $value = self::ParseStructuredValue($value, $keyLC);
            if ($type && !$isItem) {
                $value['type'] = $type;
            }
        } else {
            $value = $this->parseNonStructuredValue($keyLC, $value, $type);
        }
        return $value;
    }

    private function parseNonStructuredValue($keyLC, $value, $type)
    {
        if (in_array($keyLC, self::$Spec_MultipleValueElements)) {
            $value = self::ParseMultipleTextValue($value, $keyLC);
        }
        if ($type) {
            if (is_array($value)) {
                $value['type'] = $type;
            } else {
                $value = array(
                    'value' => $value,
                    'type' => $type
                );
            }
        }
        return $value;
    }

    private function decodeValues($paramValue, $value)
    {
        if (in_array($paramValue, array('b', 'base64'))) {
            $value = base64_decode($value);
        } elseif ($paramValue == 'quoted-printable') { // v2.1
            $value = quoted_printable_decode($value);
        }
        return $value;
    }

    private function encodeValues($paramValue, $value)
    {
        if ($paramValue != 'utf-8' && $paramValue != 'utf8') {
            $value = mb_convert_encoding($value, 'UTF-8', $paramValue);
        }
        return $value;
    }

    private function checkForAdditionalParameters($key, $parameters, $value)
    {
        // Checking files for colon-separated additional parameters (Apple's Address Book does this), for example, "X-ABCROP-RECTANGLE" for photos
        $keyLC = strtolower($key);
        if (in_array($keyLC, self::$Spec_FileElements) && isset($parameters['encoding']) && in_array($parameters['encoding'], array('b', 'base64'))) {
            // If colon is present in the value, it must contain Address Book parameters
            //	(colon is an invalid character for base64 so it shouldn't appear in valid files)
            if (strpos($value, ':') !== false) {
                $value = explode(':', $value);
                $value = array_pop($value);
            }
        }

        return $value;
    }

    public function addAttributeToDataArray($key, $itemIndex, $value)
    {
        if (!isset($this->Data[$key])) {
            $this->Data[$key] = array();
        }
        if (isset($itemIndex)) {
            if (isset($this->Data[$key][$itemIndex])) {
                if (is_array($this->Data[$key][$itemIndex])) {
                    $this->Data[$key][$itemIndex]['data'] = $value;
                } if (is_string($this->Data[$key][$itemIndex])) {
                    $formerValue = $this->Data[$key][$itemIndex];
                    $this->Data[$key][$itemIndex] = array($formerValue, 'data' => $value);
                }
            } else {
                $this->Data[$key][$itemIndex] = $value;
            }
        } else {
            if (empty($this->Data[$key]) && !in_array($key, $this->specialDataArrayKeys)) {
                $this->Data[$key] = $value;
            } else {
                $this->Data[$key][] = $value;
            }
        }
    }

    public function setVCardData($vCard)
    {
        $this->Data = $vCard;
    }

    public function writeVCard()
    {
        $text = 'BEGIN:VCARD' . self::endl;
        $text .= 'VERSION:3.0' . self::endl;

        foreach ($this->Data as $key => $values) {
            if (in_array($key, array('VERSION'))) {
                continue;
            }
            if (is_array($values)) {
                $text .= $this->writeDataFromArrays($key, $values);
            } else {
                $text .= $key . ':' . $values . self::endl;
            }
        }

        $text .= 'END:VCARD' . self::endl;
        return $text;
    }

    private function writeDataFromArrays($key, $values)
    {
        $text = '';
        $keyLC = strtolower($key);
        if (array_key_exists($keyLC, parent::$Spec_StructuredElements)) {
            $text .= strtoupper($key);
            if (isset($values['Type'])) {
                $text .= self::addMultipleTypes($values['Type']);
                unset($values['Type']);
            }
            $text .= ':' . implode(';', $values) . self::endl;
            unset($key);
        } elseif (in_array($keyLC, parent::$Spec_MultipleValueElements)) {
            $text .= strtoupper($key) . ':';
            foreach ($values as $element) {
                $text .= $element;
            }
            $text .= self::endl;
        } else {
            $text .= $this->parseStructuredElements($key, $values);
        }

        return $text;
    }

    private function parseStructuredElements($key, $value)
    {
        $text = '';
        if ($key === 'item') {
            foreach ($value as $number => $data) {
                $text .= $this->setItemsData($key . $number, $data);
            }
        } elseif ($key == 'PHOTO') {
            $text .= strtoupper($key) . ';' . 'ENCODING=' . $value['Encoding'] . self::addMultipleTypes($value['type']) . ':' . $value['value'] . self::endl;
        } else {
            foreach ($value as $item) {
                if (is_array($item) && isset($item['value']) && isset($item['type'])) {
                    $text .= strtoupper($key) . self::addMultipleTypes($item['type']) . ':' . $item['value'] . self::endl;
                } else {
                    $text .= strtoupper($key) . ':' . $item . self::endl;
                }
            }
        }

        return $text;
    }

    private function setItemsData($item, $data)
    {
        $text = $item . '.' . key($data);

        if (is_array($data) && isset($data['type'])) {
            $text .= self::addMultipleTypes($data['type']);
        }
        $keyLC = strtolower(key($data));
        if (array_key_exists($keyLC, parent::$Spec_StructuredElements)) {
            $PartArray = array();
            $value = $data[key($data)];
            foreach (parent::$Spec_StructuredElements[$keyLC] as $Part) {
                $PartArray[] = isset($value[$Part]) ? $value[$Part] : '';
            }
            $text .= ':' . implode(';', $PartArray);
        } else {
            $text .= ':' . $data[key($data)];
        }
        $text .= self::endl;

        if (isset($data['data'])) {
            $key = key($data['data']);
            $text .= $item . '.' . $key . ':' . @$data['data'][$key] . self::endl;
        }
        return $text;
    }

    private static function addMultipleTypes($value)
    {
        $text = '';
        $types = $value;
        foreach ($types as $key => $value) {
            if ($key === 'X-SERVICE-TYPE') {
                $text .= ';' . $key . '=' . $value;
            } else {
                $text .= ';type=' . $value;
            }
        }
        return $text;
    }

    protected static function ParseParameters($key, array $rawParams = null)
    {
        if (!$rawParams) {
            return array();
        }

        $parameters = self::splitParmaters($rawParams);
        $type = array();
        $result = array();

        // And each parameter is checked whether anything can/should be done because of it
        foreach ($parameters as $index => $parameter) {
            // Skipping empty elements
            if (!$parameter) {
                continue;
            }
            if (count($parameter) == 1) {
                $type = self::readParameters($key, $parameter, $rawParams, $index);
            } else {
                $data = self::getSpecialParameters($parameter, $result, $type);
                $result = $data['Result'];
                $type = $data['Type'];
            }
        }

        $result['type'] = $type;

        return $result;
    }

    protected static function splitParmaters($rawParams)
    {
        // Parameters are split into (key, value) pairs
        $parameters = array();
        foreach ($rawParams as $Item) {
            $parameters[] = explode('=', $Item, 2);
        }
        return $parameters;
    }

    protected static function readParameters($key, $parameter, $rawParams, $index)
    {
        // Handling type parameters without the explicit TYPE parameter name (2.1 valid)
        if (count($parameter) == 1) {
            $type[] = self::getSingleParameter($key, $parameter);
        } elseif (count($parameter) > 2) {
            $tempTypeParams = self::ParseParameters($key, explode(',', $rawParams[$index]));
            if ($tempTypeParams['type']) {
                $type = array_merge($type, $tempTypeParams['type']);
            }
        }

        return $type;
    }

    protected static function getSingleParameter($key, $parameter)
    {
        // Checks if the type value is allowed for the specific element
        // The second part of the "if" statement means that email elements can have non-standard types (see the spec)
        $Spec_ElementTypes = array_merge(parent::$Spec_ElementTypes, self::$Spec_ElementTypes);
        $keyLC = strtolower($key);
        if (
                (isset($Spec_ElementTypes[$keyLC]) && in_array($parameter[0], $Spec_ElementTypes[$keyLC])) ||
                ($keyLC == 'email' && is_scalar($parameter[0]))
        ) {
            return $parameter[0];
        }
    }

    protected static function getSpecialParameters($parameter, $result, $type)
    {
        switch (strtolower($parameter[0])) {
            case 'encoding':
                if (in_array($parameter[1], array('quoted-printable', 'b', 'base64'))) {
                    $result['encoding'] = $parameter[1] == 'base64' ? 'b' : $parameter[1];
                }
                break;
            case 'charset':
                $result['charset'] = $parameter[1];
                break;
            case 'type':
                $type = array_merge($type, explode(',', $parameter[1]));
                break;
            case 'x-service-type':
                $type['X-SERVICE-TYPE'] = $parameter[1];
                break;
            case 'x-abcrop-rectangle':
                $type['X-ABCROP-RECTANGLE'] = $parameter[1];
                break;
            case '-wrap-type':
                $type['-wrap-TYPE'] = $parameter[1];
                break;
            case 'value':
                if ($parameter[1] == 'URL') {
                    $result['encoding'] = 'uri';
                }
                break;
        }
        return array(
            'Result' => $result,
            'Type' => $type
        );
    }

    private function removeBackslash($text)
    {
        return str_replace(array('\:', '\;', '\,', "\n"), array(':', ';', ',', ''), $text);
    }

    public function returnData()
    {
        $data = $this->Data;
        return $data;
    }
}
