<?php
///////////////////////////////////////////////////////////////////////////

class DuneCurlException extends Exception
{
    public $errno;
    public $error;
    public $http_code;

    public function __construct($errno, $error, $http_code, $message = null)
    {
        parent::__construct(
            isset($message) ? $message :
            ($errno != 0 ? "CURL errno: $errno ($error)" :
             ($http_code != 200 ? "HTTP error: $http_code" : "OK")),
            $errno);

        $this->errno = $errno;
        $this->error = $error;
        $this->http_code = $http_code;
    }
}

class HD
{
    const INT_MIN = -2147483648;
    const INT_MAX = 2147483647;

    const DUNE_AUTH_PATH = "/tmp/dune_auth.txt";

    const INTERNET_STATUS_PATH = "/tmp/run/internet_status.txt";
    const NETWORK_MONITOR_PATH = "/tmp/run/network_monitor";
    const FORCE_CHECK_INTERNET_ON_ERROR_PATH =
        "/tmp/run/network_monitor/force_check_on_error";

    private static $with_network_monitor = null;
    private static $with_rows_api = null;
    private static $with_list_config = null;
    private static $with_builtin_pcontrol = null;

    public static function with_network_monitor()
    {
        if (!isset(HD::$with_network_monitor))
            HD::$with_network_monitor = is_dir(HD::NETWORK_MONITOR_PATH);
        return HD::$with_network_monitor;
    }

    public static function with_rows_api()
    {
        if (!isset(HD::$with_rows_api))
            HD::$with_rows_api = class_exists("PluginRowsFolderView");
        return HD::$with_rows_api;
    }

    public static function with_list_config()
    {
        if (!isset(HD::$with_list_config))
        {
            HD::$with_list_config =
                class_exists("EditListConfigActionData") &&
                defined("EDIT_LIST_CONFIG_OPT_REMOVE_UNCHECKED");
        }
        return HD::$with_list_config;
    }

    public static function with_builtin_pcontrol()
    {
        if (!isset(HD::$with_builtin_pcontrol))
        {
            HD::$with_builtin_pcontrol =
                defined('PluginTvInfo::builtin_pcontrol_enabled');
        }
        return HD::$with_builtin_pcontrol;
    }

    public static function get($map, $key, $def=null)
    {
        if (!isset($map))
            return $def;
        if (is_object($map))
            return isset($map->$key) ? $map->$key : $def;
        return isset($map[$key]) ? $map[$key] : $def;
    }

    public static function arrget($arr, $key, $def=null)
    {
        return isset($arr[$key]) ? $arr[$key] : $def;
    }

    public static function is_map($a)
    {
        return is_array($a) &&
            array_diff_key($a, array_keys(array_keys($a)));
    }

    ///////////////////////////////////////////////////////////////////////

    public static function has_attribute($obj, $n)
    {
        $arr = (array) $obj;
        return isset($arr[$n]);
    }
    ///////////////////////////////////////////////////////////////////////

    public static function get_map_element($map, $key)
    {
        return isset($map[$key]) ? $map[$key] : null;
    }

    ///////////////////////////////////////////////////////////////////////

    public static function starts_with($str, $pattern)
    {
        return strpos($str, $pattern) === 0;
    }

    ///////////////////////////////////////////////////////////////////////

    public static function format_timestamp($ts, $fmt = null)
    {
        // NOTE: for some reason, explicit timezone is required for PHP
        // on Dune (no builtin timezone info?).

        if (is_null($fmt))
            $fmt = 'Y:m:d H:i:s';

        $dt = new DateTime('@' . $ts);
        return $dt->format($fmt);
    }

    ///////////////////////////////////////////////////////////////////////

    public static function format_duration($msecs)
    {
        $n = intval($msecs);

        if (strlen($msecs) <= 0 || $n <= 0)
            return "--:--";

        $n = $n / 1000;
        $hours = $n / 3600;
        $remainder = $n % 3600;
        $minutes = $remainder / 60;
        $seconds = $remainder % 60;

        if (intval($hours) > 0)
        {
            return sprintf("%d:%02d:%02d", $hours, $minutes, $seconds);
        }
        else
        {
            return sprintf("%02d:%02d", $minutes, $seconds);
        }
    }

    ///////////////////////////////////////////////////////////////////////

    public static function encode_user_data($a, $b = null)
    {
        $media_url = null;
        $user_data = null;

        if (is_array($a) && is_null($b))
        {
            $media_url = '';
            $user_data = $a;
        }
        else
        {
            $media_url = $a;
            $user_data = $b;
        }

        if (!is_null($user_data))
            $media_url .= '||' . json_encode($user_data);

        return $media_url;
    }

    ///////////////////////////////////////////////////////////////////////

    public static function decode_user_data($media_url_str, &$media_url, &$user_data)
    {
        $idx = strpos($media_url_str, '||');

        if ($idx === false)
        {
            $media_url = $media_url_str;
            $user_data = null;
            return;
        }

        $media_url = substr($media_url_str, 0, $idx);
        $user_data = json_decode(substr($media_url_str, $idx + 2));
    }

    ///////////////////////////////////////////////////////////////////////

    public static function create_regular_folder_range($items,
        $from_ndx = 0, $total = -1, $more_items_available = false)
    {
        if ($total === -1)
            $total = $from_ndx + count($items);

        if ($from_ndx >= $total)
        {
            $from_ndx = $total;
            $items = array();
        }
        else if ($from_ndx + count($items) > $total)
        {
            array_splice($items, $total - $from_ndx);
        }

        return array
        (
            PluginRegularFolderRange::total => intval($total),
            PluginRegularFolderRange::more_items_available => $more_items_available,
            PluginRegularFolderRange::from_ndx => intval($from_ndx),
            PluginRegularFolderRange::count => count($items),
            PluginRegularFolderRange::items => $items
        );
    }

    ///////////////////////////////////////////////////////////////////////

    public static function get_internet_status()
    {
        return is_file(self::INTERNET_STATUS_PATH) ?
            (int) file_get_contents(self::INTERNET_STATUS_PATH) : -1;
    }

    public static function force_check_internet_on_error()
    {
        if (!HD::with_network_monitor())
            return;

        $tmp_path = self::FORCE_CHECK_INTERNET_ON_ERROR_PATH . ".tmp";
        file_put_contents($tmp_path, "1");
        rename($tmp_path, self::FORCE_CHECK_INTERNET_ON_ERROR_PATH);
    }

    ///////////////////////////////////////////////////////////////////////

    private static $user_agent = null;

    public static function http_init()
    {
        if (isset(self::$user_agent))
            return;

        $extra_useragent = "";

        $sysinfo = file("/tmp/sysinfo.txt", FILE_IGNORE_NEW_LINES);
        if (isset($sysinfo))
        {
            foreach ($sysinfo as $line)
            {
                if (preg_match("/product_id:/", $line) ||
                    preg_match("/firmware_version:/", $line))
                {
                    if ($extra_useragent == "")
                        $extra_useragent = " (";
                    else
                        $extra_useragent = $extra_useragent . "; ";

                    $extra_useragent = $extra_useragent . $line;
                }
            }

            if ($extra_useragent != "")
                $extra_useragent = $extra_useragent . ")";
        }

        self::$user_agent = "DuneHD/1.0" . $extra_useragent;
        hd_print("HTTP UserAgent: " . self::$user_agent);
    }

    public static function get_dune_user_agent()
    {
        return self::$user_agent;
    }

    public static function http_get_document($url, $opts = null, $silent=false)
    {
        self::http_init();

        $ch = curl_init();

        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT,    30);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER,    true);
        curl_setopt($ch, CURLOPT_TIMEOUT,           60);
        curl_setopt($ch, CURLOPT_USERAGENT,         self::get_dune_user_agent());
        curl_setopt($ch, CURLOPT_URL,               $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER,    false);     // Disabled SSL Cert checks
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION,    1);

        if (isset($opts))
        {
            foreach ($opts as $k => $v)
                curl_setopt($ch, $k, $v);
        }

        if (!$silent)
        {
            $req = HD::get($opts, CURLOPT_CUSTOMREQUEST);
            $op = isset($opts[CURLOPT_POSTFIELDS]) ? "posting" : "fetching";
            if      ($req == 'DELETE')  $op = 'deleting';
            else if ($req == 'PUT')     $op = 'putting';
            else if ($req == 'POST')    $op = 'posting';
            else if ($req == 'PATCH')   $op = 'patching';
            hd_print("HTTP $op '$url'...");
        }

        if (!$silent)
        {
            if (isset($opts) && isset($opts[CURLOPT_POSTFIELDS]))
            {
                $v = $opts[CURLOPT_POSTFIELDS];
                hd_print("HTTP POST fields: ".
                    (is_array($v) ? print_r($v,true) : "'$v'"));
            }
        }

        $start_tm = microtime(true);
        $content = curl_exec($ch);
        $execution_tm = microtime(true) - $start_tm;
        $curl_errno = curl_errno($ch);
        $curl_error = curl_error($ch);

        $len = strlen($content);

        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        // TODO: revise
        if ($content === false || $curl_errno != 0)
            HD::force_check_internet_on_error();

        if ($content === false || !in_array($http_code, array(200, 201)))
        {
            $err_msg =
                "HTTP error: $http_code; " .
                "CURL errno: " . curl_errno($ch) .
                " (" . curl_error($ch) . '), in '. sprintf("%.3fs", $execution_tm);
            if (!$silent)
                hd_print($err_msg);
            throw new DuneCurlException(
                $curl_errno, $curl_error, $http_code, $err_msg);
        }

        if (!$silent)
        {
            hd_print("HTTP OK ($http_code, $len bytes) in ".sprintf("%.3fs", $execution_tm));
            if ($len > 0)
            {
                hd_print("    Data: ".
                    str_replace("\n", "", substr($content, 0, 4096)).
                    ($len > 4096 ? "..." : ""));
            }
        }

        curl_close($ch);

        return $content;
    }

    ///////////////////////////////////////////////////////////////////////

    public static function http_post_document($url, $post_data,
        $opts = null, $silent=false)
    {
        $post_opts = array
        (
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $post_data
        );

        if (!is_null($opts))
        {
            foreach ($opts as $k => $v)
                $post_opts[$k] = $v;
        }

        return self::http_get_document($url, $post_opts, $silent);
    }

    ///////////////////////////////////////////////////////////////////////

    public static function append_dune_auth($url)
    {
        $sep = FALSE === strpos($url, '?') ? '?' : '&';
        $res = file_get_contents(self::DUNE_AUTH_PATH);
        $data = $res === false ? '' : $res;
        return "$url${sep}$data";
    }

    ///////////////////////////////////////////////////////////////////////

    public static function write_to_file_using_tmp($path, $str, $op="file")
    {
        $tmp_path = "$path.tmp";
        if (false === file_put_contents($tmp_path, $str))
        {
            hd_print("$op E: failed to write file '$tmp_path'");
            return false;
        }

        if (false === rename($tmp_path, $path))
        {
            hd_print("$op E: failed to rename to '$path'");
            unlink($tmp_path);
            return false;
        }

        return true;
    }

    ///////////////////////////////////////////////////////////////////////

    public static function write_to_file_using_tmp_if_changed($path, $str)
    {
        if (is_file($path))
        {
            $old_str = file_get_contents($path);
            if ($str == $old_str)
                return 0;
        }

        $tmp_path = "$path.tmp";
        if (false === file_put_contents($tmp_path, $str))
        {
            hd_print("Error writing to '$tmp_path'");
            return -1;
        }

        if (false === rename($tmp_path, $path))
        {
            hd_print("Error renaming '$tmp_path' to $path");
            unlink($tmp_path);
            return -1;
        }

        return 1;
    }

    public static function safe_unlink($path)
    {
        if (is_file($path))
            unlink($path);
    }

    ///////////////////////////////////////////////////////////////////////

    public static function parse_xml_document($doc)
    {
        $xml = simplexml_load_string($doc);

        if ($xml === false)
        {
            hd_print("Error: can not parse XML document.");
            hd_print("XML-text: $doc.");
            throw new Exception('Illegal XML document');
        }

        return $xml;
    }

    public static function parse_props($doc)
    {
        $props = array();

        $tok = strtok($doc, "\n");
        while ($tok !== false) {
            $pos = strpos($tok, '=');
            if ($pos)
            {
                $key = trim(substr($tok, 0, $pos));
                $val = trim(substr($tok, $pos + 1));
                $props[$key] = $val;
            }
            $tok = strtok("\n");
        }

        return $props;
    }

    public static function read_props_file($path, $def=null)
    {
        if (!$path || !is_file($path))
            return $def;

        $doc = file_get_contents($path);
        if ($doc === false)
            return $def;

        return HD::parse_props($doc);
    }

    ///////////////////////////////////////////////////////////////////////////

    public static function is_android()
    {
        return is_file("/system/dunehd/init");
    }

    ///////////////////////////////////////////////////////////////////////

    public static function make_json_rpc_request($op_name, $params)
    {
        static $request_id = 0;

        $request = array
        (
            'jsonrpc' => '2.0',
            'id' => ++$request_id,
            'method' => $op_name,
            'params' => $params
        );

        return $request;
    }

    ///////////////////////////////////////////////////////////////////////////

    public static function get_ip_addr()
    {
        preg_match_all('/inet'.(false ? '6?' : '').' addr: ?([^ ]+)/', `ifconfig`, $ips);
        if ($ips[1][0]!= '127.0.0.1')
            $ip_address = $ips[1][0];
        else if ($ips[1][1]!= '127.0.0.1')
            $ip_address = $ips[1][1];
        return $ip_address;
    }

    ///////////////////////////////////////////////////////////////////////////

    public static function get_mac_addr()
    {
        static $mac_addr = null;

        if (is_null($mac_addr))
        {
            $mac_addr = shell_exec(
                'ifconfig  eth0 | head -1 | sed "s/^.*HWaddr //"');

            $mac_addr = trim($mac_addr);

#            hd_print("MAC Address: '$mac_addr'");
        }

        return $mac_addr;
    }

    ///////////////////////////////////////////////////////////////////////////

    public static function is_apk() {
        return (bool) getenv("HD_APK");
    }

    public static function is_fw_apk() {
        return (bool) getenv("HD_FW_APK");
    }

    public static function is_limited_apk() {
        return self::is_apk() && !self::is_fw_apk();
    }

    ///////////////////////////////////////////////////////////////////////////

    public static function enable_caching($image_url)
    {
        $image_url = strval($image_url);

        if (!$image_url ||
            (substr($image_url, 0, 4) != 'http' &&
             substr($image_url, 0, 12) != 'cached_image'))
        {
            return $image_url;
        }

        $sep = FALSE === strpos($image_url, '?') ? '?' : '&';
        $image_url = $image_url . $sep . "dune_image_cache=1";
        return $image_url;
    }

    ///////////////////////////////////////////////////////////////////////////

    // TODO: localization
    private static $MONTHS = array(
        'January',
        'February',
        'March',
        'April',
        'May',
        'June',
        'July',
        'August',
        'September',
        'October',
        'November',
        'December',
    );

    public static function format_date_time_date($tm)
    {
        $lt = localtime($tm);
        $mon = self::$MONTHS[$lt[4]];
        return sprintf("%02d %s %04d", $lt[3], $mon, $lt[5] + 1900);
    }

    public static function format_date_time_time($tm, $with_sec = false)
    {
        $format = '%H:%M';
        if ($with_sec)
            $format .= ':%S';
        return strftime($format, $tm);
    }

    public static function readlines($path)
    {
        if (!is_file($path))
            return array();
        return file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    }

    public static function print_backtrace()
    {
        hd_print('Back trace:');
        foreach (debug_backtrace() as $f)
        {
            hd_print(
                '  - ' . $f['function'] . 
                ' at ' . $f['file'] . ':' . $f['line']);
        }
    }
    
    public static function escape_shell_arg($str)
    {
        return "'".str_replace("'", "'\\''", $str)."'";
    }

    public static function encode_json_str($str)
    {
        $escapers = array("\\", "\"", "\n", "\r", "\t", "\x08", "\x0c");
        $replacements = array("\\\\", "\\\"", "\\n", "\\r", "\\t", "\\f", "\\b");
        return str_replace($escapers, $replacements, $str);
    }

    public static function path_basename($str)
    {
        return preg_replace('/^.*\//', '', $str);
    }

    public static function path_dirname($str)
    {
        $str = rtrim($str, '/');
        $ndx = strrpos($str, '/');
        if ($ndx === false)
            return '';
        if ($ndx == 0)
            return '/';
        return substr($str, 0, $ndx);
    }

    public static function http_status_code_to_string($code)
    {
        // Source: http://en.wikipedia.org/wiki/List_of_HTTP_status_codes

        switch( $code )
        {
            // 1xx Informational
            case 100: $string = 'Continue'; break;
            case 101: $string = 'Switching Protocols'; break;
            case 102: $string = 'Processing'; break; // WebDAV
            case 122: $string = 'Request-URI too long'; break; // Microsoft

            // 2xx Success
            case 200: $string = 'OK'; break;
            case 201: $string = 'Created'; break;
            case 202: $string = 'Accepted'; break;
            case 203: $string = 'Non-Authoritative Information'; break; // HTTP/1.1
            case 204: $string = 'No Content'; break;
            case 205: $string = 'Reset Content'; break;
            case 206: $string = 'Partial Content'; break;
            case 207: $string = 'Multi-Status'; break; // WebDAV

            // 3xx Redirection
            case 300: $string = 'Multiple Choices'; break;
            case 301: $string = 'Moved Permanently'; break;
            case 302: $string = 'Found'; break;
            case 303: $string = 'See Other'; break; //HTTP/1.1
            case 304: $string = 'Not Modified'; break;
            case 305: $string = 'Use Proxy'; break; // HTTP/1.1
            case 306: $string = 'Switch Proxy'; break; // Depreciated
            case 307: $string = 'Temporary Redirect'; break; // HTTP/1.1

            // 4xx Client Error
            case 400: $string = 'Bad Request'; break;
            case 401: $string = 'Unauthorized'; break;
            case 402: $string = 'Payment Required'; break;
            case 403: $string = 'Forbidden'; break;
            case 404: $string = 'Not Found'; break;
            case 405: $string = 'Method Not Allowed'; break;
            case 406: $string = 'Not Acceptable'; break;
            case 407: $string = 'Proxy Authentication Required'; break;
            case 408: $string = 'Request Timeout'; break;
            case 409: $string = 'Conflict'; break;
            case 410: $string = 'Gone'; break;
            case 411: $string = 'Length Required'; break;
            case 412: $string = 'Precondition Failed'; break;
            case 413: $string = 'Request Entity Too Large'; break;
            case 414: $string = 'Request-URI Too Long'; break;
            case 415: $string = 'Unsupported Media Type'; break;
            case 416: $string = 'Requested Range Not Satisfiable'; break;
            case 417: $string = 'Expectation Failed'; break;
            case 422: $string = 'Unprocessable Entity'; break; // WebDAV
            case 423: $string = 'Locked'; break; // WebDAV
            case 424: $string = 'Failed Dependency'; break; // WebDAV
            case 425: $string = 'Unordered Collection'; break; // WebDAV
            case 426: $string = 'Upgrade Required'; break;
            case 449: $string = 'Retry With'; break; // Microsoft
            case 450: $string = 'Blocked'; break; // Microsoft

            // 5xx Server Error
            case 500: $string = 'Internal Server Error'; break;
            case 501: $string = 'Not Implemented'; break;
            case 502: $string = 'Bad Gateway'; break;
            case 503: $string = 'Service Unavailable'; break;
            case 504: $string = 'Gateway Timeout'; break;
            case 505: $string = 'HTTP Version Not Supported'; break;
            case 506: $string = 'Variant Also Negotiates'; break;
            case 507: $string = 'Insufficient Storage'; break; // WebDAV
            case 509: $string = 'Bandwidth Limit Exceeded'; break; // Apache
            case 510: $string = 'Not Extended'; break;

            // Unknown code:
            default: $string = 'Unknown'; break;
        }
        return $string;
    }
}

///////////////////////////////////////////////////////////////////////////
?>
