<?php
// Class includes
require_once('global.php');
require_once(CRAYON_PARSER_PHP);
require_once(CRAYON_FORMATTER_PHP);
require_once(CRAYON_SETTINGS_PHP);
require_once(CRAYON_LANGS_PHP);

/* The main class for managing the syntax highlighter */

class CrayonHighlighter
{
    // Properties and Constants ===============================================
    private $id = '';
    // URL is initially NULL, meaning none provided
    private $url = NULL;
    private $code = '';
    private $formatted_code = '';
    private $title = '';
    private $line_count = 0;
    private $marked_lines = array();
    private $range = NULL;
    private $error = '';
    // Determine whether the code needs to be loaded, parsed or formatted
    private $needs_load = FALSE;
    private $needs_format = FALSE;
    // Record the script run times
    private $runtime = array();
    // Whether the code is mixed
    private $is_mixed = FALSE;
    // Inline code on a single floating line
    private $is_inline = FALSE;
    private $is_highlighted = TRUE;

    // Objects
    // Stores the CrayonLang being used
    private $language = NULL;
    // A copy of the current global settings which can be overridden
    private $settings = NULL;

    // Methods ================================================================
    function __construct($url = NULL, $language = NULL, $id = NULL)
    {
        if ($url !== NULL) {
            $this->url($url);
        }

        if ($language !== NULL) {
            $this->language($language);
        }
        // Default ID
        $id = $id !== NULL ? $id : uniqid();
        $this->id($id);
    }

    /* Tries to load the code locally, then attempts to load it remotely */
    private function load()
    {
        if (empty($this->url)) {
            $this->error('The specified URL is empty, please provide a valid URL.');
            return;
        }
        // Try to replace the URL with an absolute path if it is local, used to prevent scripts
        // from executing when they are loaded.
        $url = $this->url;
        if ($this->setting_val(CrayonSettings::DECODE_ATTRIBUTES)) {
            $url = CrayonUtil::html_entity_decode($url);
        }
        $url = CrayonUtil::pathf($url);
        $site_http = CrayonGlobalSettings::site_url();
        $scheme = parse_url($url, PHP_URL_SCHEME);
        // Try to replace the site URL with a path to force local loading
        if (empty($scheme)) {
            // No url scheme is given - path may be given as relative
            $url = CrayonUtil::path_slash($site_http) . CrayonUtil::path_slash($this->setting_val(CrayonSettings::LOCAL_PATH)) . $url;
        }
        $http_code = 0;
        // If available, use the built in wp remote http get function.
        if (function_exists('wp_remote_get')) {
            $url_uid = 'crayon_' . CrayonUtil::str_uid($url);
            $cached = get_transient($url_uid, 'crayon-syntax');
            CrayonSettingsWP::load_cache();
            if ($cached !== FALSE) {
                $content = $cached;
                $http_code = 200;
            } else {
                $response = @wp_remote_get($url, array('sslverify' => false, 'timeout' => 20));
                $content = wp_remote_retrieve_body($response);
                $http_code = wp_remote_retrieve_response_code($response);
                $cache = $this->setting_val(CrayonSettings::CACHE);
                $cache_sec = CrayonSettings::get_cache_sec($cache);
                if ($cache_sec > 1 && $http_code >= 200 && $http_code < 400) {
                    set_transient($url_uid, $content, $cache_sec);
                    CrayonSettingsWP::add_cache($url_uid);
                }
            }
        } else if (in_array(parse_url($url, PHP_URL_SCHEME), array('ssl', 'http', 'https'))) {
            // Fallback to cURL. At this point, the URL scheme must be valid.
            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_HEADER, FALSE);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
            // For https connections, we do not require SSL verification
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
            curl_setopt($ch, CURLOPT_FRESH_CONNECT, FALSE);
            curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
            if (isset($_SERVER['HTTP_USER_AGENT'])) {
                curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
            }
            $content = curl_exec($ch);
            $error = curl_error($ch);
            $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
        }
        if ($http_code >= 200 && $http_code < 400) {
            $this->code($content);
        } else {
            if (empty($this->code)) {
                // If code is also given, just use that
                $this->error("The provided URL ('$this->url'), parsed remotely as ('$url'), could not be accessed.");
            }
        }
        $this->needs_load = FALSE;
    }

    /* Central point of access for all other functions to update code. */
    public function process()
    {
        $tmr = new CrayonTimer();
        $this->runtime = NULL;
        if ($this->needs_load) {
            $tmr->start();
            $this->load();
            $this->runtime[CRAYON_LOAD_TIME] = $tmr->stop();
        }
        if (!empty($this->error) || empty($this->code)) {
            // Disable highlighting for errors and empty code
            return;
        }

        if ($this->language === NULL) {
            $this->language_detect();
        }
        if ($this->needs_format) {
            $tmr->start();
            try {
                // Parse before hand to read modes
                $code = $this->code;
                // If inline, then combine lines into one
                if ($this->is_inline) {
                    $code = preg_replace('#[\r\n]+#ms', '', $code);
                    if ($this->setting_val(CrayonSettings::TRIM_WHITESPACE)) {
                        $code = trim($code);
                    }
                }
                // Decode html entities (e.g. if using visual editor or manually encoding)
                if ($this->setting_val(CrayonSettings::DECODE)) {
                    $code = CrayonUtil::html_entity_decode($code);
                }
                // Save code so output is plain output is the same
                $this->code = $code;

                // Allow mixed if langauge supports it and setting is set
                CrayonParser::parse($this->language->id());
                if (!$this->setting_val(CrayonSettings::MIXED) || !$this->language->mode(CrayonParser::ALLOW_MIXED)) {
                    // Format the code with the generated regex and elements
                    $this->formatted_code = CrayonFormatter::format_code($code, $this->language, $this);
                } else {
                    // Format the code with Mixed Highlighting
                    $this->formatted_code = CrayonFormatter::format_mixed_code($code, $this->language, $this);
                }
            } catch (Exception $e) {
                $this->error($e->message());
                return;
            }
            $this->needs_format = FALSE;
            $this->runtime[CRAYON_FORMAT_TIME] = $tmr->stop();
        }
    }

    /* Used to format the glue in between code when finding mixed languages */
    private function format_glue($glue, $highlight = TRUE)
    {
        // TODO $highlight
        return CrayonFormatter::format_code($glue, $this->language, $this, $highlight);
    }

    /* Sends the code to the formatter for printing. Apart from the getters and setters, this is
     the only other function accessible outside this class. $show_lines can also be a string. */
    function output($show_lines = TRUE, $print = TRUE)
    {
        $this->process();
        if (empty($this->error)) {
            // If no errors have occured, print the formatted code
            $ret = CrayonFormatter::print_code($this, $this->formatted_code, $show_lines, $print);
        } else {
            $ret = CrayonFormatter::print_error($this, $this->error, '', $print);
        }
        // Reset the error message at the end of the print session
        $this->error = '';
        // If $print = FALSE, $ret will contain the output
        return $ret;
    }

    // Getters and Setters ====================================================
    function code($code = NULL)
    {
        if ($code === NULL) {
            return $this->code;
        } else {
            // Trim whitespace
            if ($this->setting_val(CrayonSettings::TRIM_WHITESPACE)) {
                $code = preg_replace("#(?:^\\s*\\r?\\n)|(?:\\r?\\n\\s*$)#", '', $code);
            }

            if ($this->setting_val(CrayonSettings::TRIM_CODE_TAG)) {
                $code = preg_replace('#^\s*<\s*code[^>]*>#msi', '', $code);
                $code = preg_replace('#</\s*code[^>]*>\s*$#msi', '', $code);
            }

            $before = $this->setting_val(CrayonSettings::WHITESPACE_BEFORE);
            if ($before > 0) {
                $code = str_repeat("\n", $before) . $code;
            }
            $after = $this->setting_val(CrayonSettings::WHITESPACE_AFTER);
            if ($after > 0) {
                $code = $code . str_repeat("\n", $after);
            }

            if (!empty($code)) {
                $this->code = $code;
                $this->needs_format = TRUE;
            }
        }
    }

    function language($id = NULL)
    {
        if ($id === NULL || !is_string($id)) {
            return $this->language;
        }

        if (($lang = CrayonResources::langs()->get($id)) != FALSE || ($lang = CrayonResources::langs()->alias($id)) != FALSE) {
            // Set the language if it exists or look for an alias
            $this->language = $lang;
        } else {
            $this->language_detect();
        }

        // Prepare the language for use, even if we have no code, we need the name
        CrayonParser::parse($this->language->id());
    }

    function language_detect()
    {
        // Attempt to detect the language
        if (!empty($id)) {
            $this->log("The language '$id' could not be loaded.");
        }
        $this->language = CrayonResources::langs()->detect($this->url, $this->setting_val(CrayonSettings::FALLBACK_LANG));
    }

    function url($url = NULL)
    {
        if ($url === NULL) {
            return $this->url;
        } else {
            $this->url = $url;
            $this->needs_load = TRUE;
        }
    }

    function title($title = NULL)
    {
        if (!CrayonUtil::str($this->title, $title)) {
            return $this->title;
        }
    }

    function line_count($line_count = NULL)
    {
        if (!CrayonUtil::num($this->line_count, $line_count)) {
            return $this->line_count;
        }
    }

    function marked($str = NULL)
    {
        if ($str === NULL) {
            return $this->marked_lines;
        }
        // If only an int is given
        if (is_int($str)) {
            $array = array($str);
            return CrayonUtil::arr($this->marked_lines, $array);
        }
        // A string with ints separated by commas, can also contain ranges
        $array = CrayonUtil::trim_e($str);
        $array = array_unique($array);
        $lines = array();
        foreach ($array as $line) {
            // Check for ranges
            if (strpos($line, '-') !== FALSE) {
                $ranges = CrayonUtil::range_str($line);
                $lines = array_merge($lines, $ranges);
            } else {
                // Otherwise check the string for a number
                $line = intval($line);
                if ($line !== 0) {
                    $lines[] = $line;
                }
            }
        }
        return CrayonUtil::arr($this->marked_lines, $lines);
    }

    function range($str = NULL)
    {
        if ($str === NULL) {
            return $this->range;
        } else {
            $range = CrayonUtil::range_str_single($str);
            if ($range) {
                $this->range = $range;
            }
        }
        return FALSE;
    }

    function log($var)
    {
        if ($this->setting_val(CrayonSettings::ERROR_LOG)) {
            CrayonLog::log($var);
        }
    }

    function id($id = NULL)
    {
        if ($id == NULL) {
            return $this->id;
        } else {
            $this->id = strval($id);
        }
    }

    function error($string = NULL)
    {
        if (!$string) {
            return $this->error;
        }
        $this->error .= $string;
        $this->log($string);
        // Add the error string and ensure no further processing occurs
        $this->needs_load = FALSE;
        $this->needs_format = FALSE;
    }

    // Set and retreive settings
    // TODO fix this, it's too limiting
    function settings($mixed = NULL)
    {
        if ($this->settings == NULL) {
            $this->settings = CrayonGlobalSettings::get_obj();
        }

        if ($mixed === NULL) {
            return $this->settings;
        } else if (is_string($mixed)) {
            return $this->settings->get($mixed);
        } else if (is_array($mixed)) {
            $this->settings->set($mixed);
            return TRUE;
        }
        return FALSE;
    }

    /* Retrieve a single setting's value for use in the formatter. By default, on failure it will
     * return TRUE to ensure FALSE is only sent when a setting is found. This prevents a fake
     * FALSE when the formatter checks for a positive setting (Show/Enable) and fails. When a
     * negative setting is needed (Hide/Disable), $default_return should be set to FALSE. */
    // TODO fix this (see above)
    function setting_val($name = NULL, $default_return = TRUE)
    {
        if (is_string($name) && $setting = $this->settings($name)) {
            return $setting->value();
        } else {
            // Name not valid
            return (is_bool($default_return) ? $default_return : TRUE);
        }
    }

    // Set a setting value
    // TODO fix this (see above)
    function setting_set($name = NULL, $value = TRUE)
    {
        $this->settings->set($name, $value);
    }

    // Used to find current index in dropdown setting
    function setting_index($name = NULL)
    {
        $setting = $this->settings($name);
        if (is_string($name) && $setting->is_array()) {
            return $setting->index();
        } else {
            // Returns -1 to avoid accidentally selecting an item in a dropdown
            return CrayonSettings::INVALID;
        }
    }

    function formatted_code()
    {
        return $this->formatted_code;
    }

    function runtime()
    {
        return $this->runtime;
    }

    function is_highlighted($highlighted = NULL)
    {
        if ($highlighted === NULL) {
            return $this->is_highlighted;
        } else {
            $this->is_highlighted = $highlighted;
        }
    }

    function is_mixed($mixed = NULL)
    {
        if ($mixed === NULL) {
            return $this->is_mixed;
        } else {
            $this->is_mixed = $mixed;
        }
    }

    function is_inline($inline = NULL)
    {
        if ($inline === NULL) {
            return $this->is_inline;
        } else {
            $inline = CrayonUtil::str_to_bool($inline, FALSE);
            $this->is_inline = $inline;
        }
    }
}