<?php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use it\thecsea\simple_caldav_client\SimpleCalDAVClient;
use it\thecsea\simple_caldav_client\CalDAVException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\ORM\EntityManagerInterface;

use App\Entity\Calendarevent;


class CalFileParser {

    private $_base_path = './';
    private $_file_name = '';
    private $_output = 'array';
    private $DTfields = array('DTSTART', 'DTEND', 'DTSTAMP', 'CREATED', 'EXDATE', 'LAST-MODIFIED');
    private $_user_timezone = null;
    private $_file_timezone = null;
    private $ctx;

    function __construct() {
        $this->_default_output = $this->_output;
        $this->ctx = stream_context_create(array('http'=>array('timeout' => 5)));
    }

    public function set_base_path($path) {
        if (isset($path)) {
            $this->_base_path = $path;
        }
    }

    public function set_file_name($filename) {
        if (!empty($filename)) {
            $this->_file_name = $filename;
        }
    }

    public function set_output($output) {
        if (!empty($output)) {
            $this->_output = $output;
        }
    }

    public function set_timezone($timezone) {
        if (!empty($timezone)) {
            $this->_user_timezone = $timezone;
        }
    }

    public function get_base_path() {
        return $this->_base_path;
    }

    public function get_file_name() {
        return $this->_file_name;
    }

    public function get_output() {
        return $this->_output;
    }

    /**
     * Read File
     *
     * @param string $file
     * @return string
     *
     * @example
     *  read_file('schedule.vcal')
     *  read_file('../2011-08/'schedule.vcal');
     *  read_file('http://michaelencode.com/example.vcal');
     */
    public function read_file($file = '', $options = array()) {
        

        if (empty($file)) {
            $file = $this->_file_name;
        }

        // check to see if file path is a url
        if (preg_match('/^(http|https):/', $file) === 1) {
            return $this->read_remote_file($file,$options);
        }

        //empty base path if file starts with forward-slash
        if (substr($file, 0, 1) === '/') {
            $this->set_base_path('');
        }

        if (!empty($file) && file_exists($this->_base_path . $file)) {
            try {
                @$file_contents = file_get_contents($this->_base_path . $file,false,$this->ctx);
                return $file_contents;
            }
            catch (Exception $e) {
                return false;
            }
        } else {
            return false;
        }
    }

    /**
     * Read Remote File
     * @param $file
     * @return bool|string
     */
    public function read_remote_file($file, $options=array()) {
        if (!empty($file)) {
            if(isset($options["proxy"])) {
                $aContext = array(
                    'http' => array(
                        'proxy' => 'http://'.$options["proxy"],
                        'request_fulluri' => true,
                        'timeout' => 5
                    ),
                );
                $cxContext = stream_context_create($aContext);
                try {
                    @$data = file_get_contents($file, False, $cxContext);
                }
                catch (Exception $e) {
                    return false;
                }                
            }
            else {
                try {
                    @$data = file_get_contents($file,false,$this->ctx);
                }
                catch (Exception $e) {
                    return false;
                }                
            }                
            
            if ($data !== false) {
                return $data;
            }
        }
        return false;
    }

    /**
     * Parse
     * Parses iCal or vCal file and returns data of a type that is specified
     * @param string $file
     * @param string $output
     * @return mixed|string
     */
    public function parse($file = '', $output = '', $options = array()) {
        $file_contents = $this->read_file($file,$options);

        if ($file_contents === false) {
            return 'Error: File Could not be read';
        }

        if (empty($output)) {
            $output = $this->_output;
        }

        if (empty($output)) {
            $output = $this->_default_output;
        }

        $events_arr = array();


        // fetch timezone to create datetime object
        if (preg_match('/X-WR-TIMEZONE:(.+)/i', $file_contents, $timezone) === 1) {
            $this->_file_timezone = trim($timezone[1]);
            if ($this->_user_timezone == null) {
                $this->_user_timezone = $this->_file_timezone;
            }
        } else {
            $this->_file_timezone = $this->_user_timezone;
        }

        // tell user if setting timezone is necessary
        if ($this->_user_timezone == null) {
            return 'Error: no timezone set or found';
        }

        //put contains between start and end of VEVENT into array called $events
        preg_match_all('/(BEGIN:VEVENT.*?END:VEVENT)/si', $file_contents, $events);

        if (!empty($events)) {
            foreach ($events[0] as $event_str) {

                //remove begin and end "tags"
                $event_str = trim(str_replace(array('BEGIN:VEVENT','END:VEVENT'),'',$event_str));

                //convert string of entire event into an array with elements containing string of 'key:value'
                $event_key_pairs = $this->convert_event_string_to_array($event_str);

                //convert array of 'key:value' strings to an array of key => values
                $events_arr[] = $this->convert_key_value_strings($event_key_pairs);
            }
        }

        $this->_output = $this->_default_output;

        return $this->output($events_arr, $output);
    }

    /**
     * Output
     * outputs data in the format specified
     *
     * @param $events_arr
     * @param string $output
     * @return mixed
     */
    private function output($events_arr, $output = 'array') {
        switch ($output) {
            case 'json' :
                return json_encode($events_arr);
                break;
            default :
                return $events_arr;
                break;
        }
    }

    /**
     * Convert event string to array
     * accepts a string of calendar event data and produces array of 'key:value' strings
     * See convert_key_value_strings() to convert strings to
     * @param string $event_str
     * @return array
     */
    private function convert_event_string_to_array($event_str = '') {
        if (!empty($event_str)) {
            //replace new lines with a custom delimiter
            $event_str = preg_replace("/[\r\n]/", "%%" ,$event_str);

            // take care of line wrapping
            $event_str = preg_replace("/%%%% /", "" ,$event_str);

            if (strpos(substr($event_str, 2), '%%') == '0') { //if this code is executed, then file consisted of one line causing previous tactic to fail
                $tmp_piece = explode(':',$event_str);
                $num_pieces = count($tmp_piece);

                $event_str = '';
                foreach ($tmp_piece as $key => $item_str) {

                    if ($key != ($num_pieces -1) ) {

                        //split at spaces
                        $tmp_pieces = preg_split('/\s/',$item_str);

                        //get the last whole word in the string [item]
                        $last_word = end($tmp_pieces);

                        //adds delimiter to front and back of item string, and also between each new key
                        $item_str = trim(str_replace(array($last_word,' %%' . $last_word),array('%%' . $last_word . ':', '%%' . $last_word), $item_str));
                    }

                    //build the event string back together, piece by piece
                    $event_str .= trim($item_str);
                }
            }

            //perform some house cleaning just in case
            $event_str = str_replace('%%%%','%%', $event_str);

            if (substr($event_str, 0, 2) == '%%') {
                $event_str = substr($event_str, 2);
            }

            //break string into array elements at custom delimiter
            $return = explode('%%',$event_str);
        } else {
            $return = array();
        }

        return $return;
    }

    /**
     * Parse Key Value String
     * accepts an array of strings in the format of 'key:value' and returns an array of keys and values
     * @param array $event_key_pairs
     * @return array
     */
    private function convert_key_value_strings($event_key_pairs = array()) {
        $event = array();
        $event_alarm = array();
        $event_alarms = array();
        $inside_alarm = false;

        if (!empty($event_key_pairs)) {
            foreach ($event_key_pairs as $line) {

                if (empty($line)) continue;

                $line_data = explode(':', $line, 2);
                $key = trim((isset($line_data[0])) ? $line_data[0] : "");
                $value = trim((isset($line_data[1])) ? $line_data[1] : "");

                // we are parsing an alarm for this event
                if ($key == "BEGIN" && $value == "VALARM") {
                    $inside_alarm = true;
                    $event_alarm = array();
                    continue;
                }

                // we finished parsing an alarm for this event
                if ($key == "END" && $value == "VALARM") {
                    $inside_alarm = false;
                    $event_alarms[] = $event_alarm;
                    continue;
                }

                // autoconvert datetime fields to DateTime object
                $date_key = (strstr($key,";")) ? strstr($key,";", true) : $key;
                $date_format = (strstr($key,";")) ? strstr($key,";") : ";VLAUE=DATE-TIME";

                if (in_array($date_key, $this->DTfields)) {

                    // set date key without format
                    $key = $date_key;

                    $timezone = $this->_file_timezone;

                    // found time zone in date format info
                    if (strstr($date_format,"TZID")) $timezone = substr($date_format, 5);
                    $timezone=str_replace("=","",$timezone);

                    // process all dates if there are more then one and comma seperated
                    $processed_value = array();
                    foreach(explode(",", $value) AS $date_value) {

                        // this is simply a date
                        if ($date_format == ";VALUE=DATE") $date_value .= "T000000";

                        // date-time in UTC
                        if (substr($date_value, -1) == "Z") $timezone = "UTC";

                        // format date
                        $date = \DateTime::createFromFormat('Ymd\THis', str_replace('Z', '', $date_value), new \DateTimeZone($timezone));
                        if ($date !== false) $date->setTimezone(new \DateTimeZone($this->_user_timezone));

                        if ($date !== false) $processed_value[] = $date;
                    }

                    // we have more then one date value then return it as an array
                    if (count($processed_value) > 1) {
                        $value = $processed_value;
                    } else {
                        if ($date !== false) $value = $date;
                    }
                }

                // check if current key was already set
                // if this is the case then add value data and turn it into an array
                $value_current_key = false;
                if ($inside_alarm) {
                    if (isset($event_alarm[$key])) $value_current_key = $event_alarm[$key];
                } else {
                    if (isset($event[$key])) $value_current_key = $event[$key];
                }

                // this current key already has data add more
                if ($value_current_key !== false) {

                    // check if data is array and merge
                    if (is_array($value_current_key)) {
                        if (is_array($value)) {
                            $value = array_merge($value_current_key, $value);
                        } else {
                            $value = array_merge($value_current_key, array($value));
                        }
                    } else {
                        if (is_array($value)) {
                            $value = array_merge(array($value_current_key), $value);
                        } else {
                            $value = array($value_current_key, $value);
                        }
                    }
                }

                if ($inside_alarm) {
                    $event_alarm[$key] = $value;
                } else {
                    $event[$key] = $value;
                }
            }
        }

        // add alarm data
        $event["VALARM"] = $event_alarms;

        // unescape every element if string.
        return array_map(function($value) {
            return (is_string($value) ? stripcslashes($value) : $value);
        }, $event);
    }
}

class SynchroICSCommand extends Command
{
    private $container;
    private $em;
    private $output;
    private $filesystem;
    private $rootlog;


    public function __construct(ContainerInterface $container, EntityManagerInterface $em)
    {
        parent::__construct();
        $this->container = $container;
        $this->em = $em;
    }

    protected function configure()
    {
        $this
            ->setName('app:SynchroICS')
            ->setDescription('Synchronize external ICS URL')
            ->setHelp('Synchronize external ICS URL')
            ->addArgument('cronid', InputArgument::OPTIONAL, 'ID Cron Job') 
            ->addArgument('lastchance', InputArgument::OPTIONAL, 'Lastchance to run the cron') 
            ->addArgument('idcalendar', InputArgument::OPTIONAL, 'ID Calendar to synchronize')             
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $this->output       = $output;
        $this->filesystem   = new Filesystem();
        $this->rootlog      = $this->container->get('kernel')->getProjectDir()."/var/log/";

        $idcalendar = $input->getArgument('idcalendar');


        $PROXYserver    = $this->em->getRepository("App\Entity\Config")->findOneBy(["id"=>"PROXYserver"])->getValue();
        $PROXYport      = $this->em->getRepository("App\Entity\Config")->findOneBy(["id"=>"PROXYport"])->getValue();

        $this->writelnred('');
        $this->writelnred('== app:SynchroICS');        
        $this->writelnred('==========================================================================================================');

        $now=new \DateTime('now');
        $fgdebug = false;

        
        if($idcalendar=="")
            $calendars=$this->em->getRepository("App\Entity\Calendar")->findAll();
        else
            $calendars=$this->em->getRepository("App\Entity\Calendar")->findBy(["id"=>$idcalendar]);

        foreach($calendars as $calendar) {

            switch($calendar->getType()) {
                // ICS
                case 1:
                    $this->writeln($calendar->getId()." = ICS = ".$calendar->getUrl());
                    $client = new CalFileParser();
                    $PROXYactivate  = $this->em->getRepository("App\Entity\Config")->findOneBy(["id"=>"PROXYactivate"])->getValue();

                    try {
                        if($PROXYactivate)
                            $cal = $client->parse($calendar->getUrl(), '', array("proxy"=>$PROXYserver.":".$PROXYport));
                        else
                            $cal = $client->parse($calendar->getUrl());
                        
                        if(!is_array($cal)) {
                            $this->writelnred("Impossible de récupérer le calendrier");
                            $this->writeln("");
                            break;
                        }

                        $tbuid=array();
                        foreach($cal as $event) {
                            $event["SUMMARY"]       = (isset($event["SUMMARY"])?$event["SUMMARY"]:"");
                            $event["DESCRIPTION"]   = (isset($event["DESCRIPTION"])?$event["DESCRIPTION"]:"");
                            $event["DTSTART"]       = (isset($event["DTSTART"])?$event["DTSTART"]:"");
                            $event["DTEND"]         = (isset($event["DTEND"])?$event["DTEND"]:"");
                            $event["ALLDAY"]        = (isset($event["ALLDAY"])?$event["ALLDAY"]:"");
                            $event["SEQUENCE"]      = (isset($event["SEQUENCE"])?$event["SEQUENCE"]:"");
                            $event["UID"]           = (isset($event["UID"])?$event["UID"]:"");
            
                            if($fgdebug) {
                                echo "<br>";
                                echo "<br>SUMMARY       = ".$event["SUMMARY"];
                                echo "<br>DESCRIPTION   = ".$event["DESCRIPTION"];
                                echo "<br>DTSTART       = ".$event["DTSTART"]->format('d/m/Y H:i:s');
                                echo "<br>DTEND         = ".$event["DTEND"]->format('d/m/Y H:i:s');
                                echo "<br>ALLDAY        = ".$event["ALLDAY"];
                                echo "<br>SEQUENCE      = ".$event["SEQUENCE"];
                                echo "<br>UID           = ".$event["UID"];
                            }

                            if($event["UID"]!="") {
                                // On sauvegarde l'uid
                                array_push($tbuid,$event["UID"]);
            
                                // Allday ?
                                $allday=0;
                                if($event["ALLDAY"]==1||$event["SEQUENCE"]>=1)
                                    $allday=1;
            
                                // Cas particulier
                                if($allday==0&&$event["SEQUENCE"]==0) {
                                    if($event["DTSTART"]->format('H:i:s')=='00:00:00'&&$event["DTEND"]->format('H:i:s')=='00:00:00')
                                        $allday=1;
                                }
            
                                // On recherche l'uid dans le calendrier
                                $toset=false;
                                $calendarevent=$this->em->getRepository("App\Entity\Calendarevent")->findOneBy(["uid"=>$event["UID"]]);
                                if($calendarevent) {
                                    if($event["SUMMARY"]!=$calendarevent->getName()||$event["DESCRIPTION"]!=$calendarevent->getDescription()||$event["DTSTART"]!=$calendarevent->getStart()||$event["DTEND"]!=$calendarevent->getEnd()||$allday!=$calendarevent->getAllday()) {
                                        $toset=true;
                                    }
                                }
                                else {
                                    $toset=true;
                                    $calendarevent=new Calendarevent();
                            }

                            if($toset) {
                                    $calendarevent->setName($event["SUMMARY"]);
                                    $calendarevent->setDescription($event["DESCRIPTION"]);
                                    $calendarevent->setStart($event["DTSTART"]);
                                    $calendarevent->setEnd($event["DTEND"]);
                                    $calendarevent->setAllday($allday);
                                    $calendarevent->setUid($event["UID"]);
                                    $calendarevent->setCalendar($calendar);
                                    
                                    $this->em->persist($calendarevent);
                                    $this->em->flush();
                                    
                            }
                            }
                        }
            
                        // Purge des évènements plus présent
                        $calendarevents=$this->em->getRepository("App\Entity\Calendarevent")->findBy(["calendar"=>$calendar]);
                        foreach($calendarevents as $calendarevent) {
                            if(!in_array($calendarevent->getUid(),$tbuid)) {
                                $this->em->remove($calendarevent);
                                $this->em->flush();                             
                            }
                        }
                    }   
                    catch (Exception $e) {
                        $this->writelnred($e->__toString());
                    }
                break;

                // CalDav
                case 2:
                    if(empty($calendar->getUrl())) break;
                    // Création du client calDAV
                    $this->writeln("");
                    $this->writeln($calendar->getId()." = CalDAV = ".$calendar->getUrl()); 

                    // Récupération du mode proxy
                    $PROXYactivate  = $this->em->getRepository("App\Entity\Config")->findOneBy(["id"=>"PROXYactivate"])->getValue();

                    
                    // En mode proxy on tente la connection via le proxy
                    if($PROXYactivate) {
                        $client = new SimpleCalDAVClient();
                        try {
                            @$client->connect($calendar->getUrl(), $calendar->getLogin(), $calendar->getPasswordDecrypt(),['proxy_host'=>$PROXYserver.":".$PROXYport,'timeout'=>5]);
                        }
                        catch (CalDAVException $e) {
                            // Si KO on tente la meme connection mais sans le proxy
                            $PROXYactivate = false;
                            $this->writeln("Tentative de connection sans proxy");
                        }
                    }     
        
                    if(!$PROXYactivate) {

                        $client = new SimpleCalDAVClient();
                        try {
                            @$client->connect($calendar->getUrl(), $calendar->getLogin(), $calendar->getPasswordDecrypt(),['timeout'=>5]);
                        }
                        catch (CalDAVException $e) {
                            $this->writelnred("");
                            $this->writelnred("KO = ".$e->getMessage());
                            break;
                        }
                    }     

                    $this->writeln("Connected");
                    $tbuid=array();
                    $arrayOfCalendars = $client->findCalendars();
                    foreach($arrayOfCalendars as $calcalendar) {
                        $this->writeln($calcalendar->getDisplayName());
                        if($calcalendar->getDisplayName()!=$calendar->getCaldavname()) continue;

                        $client->setCalendar($calcalendar);
                        $events = $client->getEvents();

                        foreach ($events as $calevent) {
                            $event=$this->icsToArray($calevent->getData())[2];
                            
                            foreach($event as $key => $value) {
                                if (strpos($key, 'DTSTART') === 0) $event["DTSTART"]=new \datetime($value);
                                if (strpos($key, 'DTEND') === 0) $event["DTEND"]=new \datetime($value);
                            }

                            $event["SUMMARY"]       = (isset($event["SUMMARY"])?$event["SUMMARY"]:"");
                            $event["DESCRIPTION"]   = (isset($event["DESCRIPTION"])?$event["DESCRIPTION"]:"");
                            $event["ALLDAY"]        = (isset($event["ALLDAY"])?$event["ALLDAY"]:"");
                            $event["SEQUENCE"]      = (isset($event["SEQUENCE"])?$event["SEQUENCE"]:"");
                            $event["UID"]           = (isset($event["UID"])?$event["UID"]:"");
                            $event["DURATION"]      = (isset($event["DURATION"])?$event["DURATION"]:"");

                            if($event["UID"]!="") {
                                // On sauvegarde l'uid
                                array_push($tbuid,$event["UID"]);

                                if(!isset($event["DTEND"])&&isset($event["DURATION"])) {
                                    $event["DTEND"]=clone $event["DTSTART"];
                                    $event["DTEND"]->add(new \DateInterval($event["DURATION"]));
                                }


                                // Allday ?
                                $allday=0;
                                if($event["ALLDAY"]==1||$event["SEQUENCE"]>=1)
                                    $allday=1;
            
                                // Cas particulier
                                if($allday==0&&$event["SEQUENCE"]==0) {
                                    if($event["DTSTART"]->format('H:i:s')=='00:00:00'&&$event["DTEND"]->format('H:i:s')=='00:00:00')
                                        $allday=1;
                                }
            
                                // On recherche l'uid dans le calendrier
                                $toset=false;
                                $calendarevent=$this->em->getRepository("App\Entity\Calendarevent")->findOneBy(["uid"=>$event["UID"]]);
                                if($calendarevent) {
                                    if($event["SUMMARY"]!=$calendarevent->getName()||$event["DESCRIPTION"]!=$calendarevent->getDescription()||$event["DTSTART"]!=$calendarevent->getStart()||$event["DTEND"]!=$calendarevent->getEnd()||$allday!=$calendarevent->getAllday()) {
                                        $toset=true;
                                    }
                                }
                                else {
                                    $toset=true;
                                    $calendarevent=new Calendarevent();
                                }

                                if($toset) {
                                        $calendarevent->setName($event["SUMMARY"]);
                                        $calendarevent->setDescription($event["DESCRIPTION"]);
                                        $calendarevent->setStart($event["DTSTART"]);
                                        $calendarevent->setEnd($event["DTEND"]);
                                        $calendarevent->setAllday($allday);
                                        $calendarevent->setUid($event["UID"]);
                                        $calendarevent->setCalendar($calendar);
                                        
                                        $this->em->persist($calendarevent);
                                        $this->em->flush();
                                        
                                }
                            }
                        }
            
                        // Purge des évènements plus présent
                        $calendarevents=$this->em->getRepository("App\Entity\Calendarevent")->findBy(["calendar"=>$calendar]);
                        foreach($calendarevents as $calendarevent) {
                            if(!in_array($calendarevent->getUid(),$tbuid)) {
                                $this->em->remove($calendarevent);
                                $this->em->flush();                             
                            }
                        }                            
                    }

                break;
            }                       
        }

        $this->writeln('');
        return Command::SUCCESS;
    }

    private function icsToArray($icsFile) {
        $icsData = explode("BEGIN:", $icsFile);

        foreach($icsData as $key => $value) {
            $icsDatesMeta[$key] = explode("\n", $value);
        }

        foreach($icsDatesMeta as $key => $value) {
            foreach($value as $subKey => $subValue) {
                if ($subValue != "") {
                    if ($key != 0 && $subKey == 0) {
                        $icsDates[$key]["BEGIN"] = $subValue;
                    } else {
                        //dump($subValue);
                        $subValueArr = explode(":", $subValue, 2);
                        //dump($subValueArr);
                        if(key_exists(1,$subValueArr)) $icsDates[$key][$subValueArr[0]] = $subValueArr[1];
                    }
                }
            }
        }

        return $icsDates;
    }

    private function writelnred($string) { 
        $this->output->writeln('<fg=red>'.$string.'</>');
        $this->filesystem->appendToFile($this->rootlog.'cron.log', $string."\n");
    }
    private function writeln($string) { 
        $this->output->writeln($string);
        $this->filesystem->appendToFile($this->rootlog.'cron.log', $string."\n");
    }     
}
