<?php

if(!defined('ABSPATH')) die('-1');

class AwesomeLiveChat_Library extends AwesomeLiveChat
{
    /**
     * Boots object
     *
     * @since  1.0.0
     * @access private
     * @var object
     */
    private $Boots;

    /**
     * Settings/Options
     *
     * @since  1.0.0
     * @access private
     * @var array
     */
    private $Options;

    /**
     * Utility
     *
     * @since  1.0.0
     * @access private
     * @var object
     */
    private $Utility;

    /**
     * Departments table name
     *
     * @since  1.0.0
     * @access private
     * @var string
     */
    private $tbl_departments;

    /**
     * Users table name
     *
     * @since  1.0.0
     * @access private
     * @var string
     */
    private $tbl_users;

    /**
     * Chat table name
     *
     * @since  1.0.0
     * @access private
     * @var string
     */
    private $tbl_chat;

    /**
     * Plugin default settings
     *
     * @since  1.0.0
     * @access private
     * @var array
     */
    static private $Default_Settings = array();

    /**
     * Plugin settings
     *
     * @since  1.0.0
     * @access private
     * @var array
     */
    static private $Settings = array();

    /**
     * Get the Boots framework object.
     * Get the plugin settings.
     * Set properties
     * Set default settings
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat::boots()
     *         return boots object
     * @uses   AwesomeLiveChat::options()
     *         return options
     * @access public
     * @param  void
     * @return void
     */
    public function __construct()
    {
        $this->Boots   = $this->boots();
        $this->Options = $this->options();
        $this->Utility = new AwesomeLiveChat_Utility();
        $this->tbl_departments = $this->Options['ALC_TABLE_DEPARTMENTS'];
        $this->tbl_users = $this->Options['ALC_TABLE_USERS'];
        $this->tbl_chat = $this->Options['ALC_TABLE_CHAT'];

        if(!self::$Default_Settings) $this->setDefaultSettings();
        if(!self::$Settings)
            self::$Settings = $this->Boots->Database
                ->term('alc_settings', array())
                ->get();
        add_action('awesome_live_chat_install', array($this, 'factory'));
    }

    /**
     * Set default plugin settings
     *
     * @since  1.0.0
     * @access private
     * @param  void
     * @return void
     */
    private function setDefaultSettings()
    {
        $timeout = ini_get('max_execution_time');
        $timeout = $timeout ? ($timeout - 5) : 3;
        $timeout = $timeout > 3 ? 3 : $timeout;
        self::$Default_Settings = array(
            'about' => array(
                'power' => ''
            ), 'block' => '',
            'general' => array(
                'mode' => 'online',
                'placement' => array('post', 'page', 'archive', 'home'),
                'responsive' => 'yes',
                'icon' => '',
                'avatar' => '',
                'sound' => 'yes',
                'position' => 'right',
                'width'  => 420,
                'height' => 300,
                'timeout' => $timeout,
                'show_offline' => 'yes',
                'email_field'  => 'required',
                'width_border' => 5,
                'hide_border' => 'no',
                'hide_status' => array(),
                'hide_website'=> array(),
                'hide_dept'   => array(),
                'bg' => 'no',
                'glass' => 'no',
                'restrict_op' => 'no',
                'iframe' => 'no'
            ), 'mail' => array(
                'offline_subject' => 'Offline message via Awesome Live Chat',
                'offline_receiver_email' => $this->Boots->Database
                    ->term('admin_email')
                    ->get(),
                'offline_sender_name' => '',
                'offline_sender_email' => '',
                'transcript_subject' => 'Chat transcript via Awesome Live Chat',
                'transcript_sender_name' => $this->Boots->Database
                    ->term('blogname')
                    ->get(),
                'transcript_sender_email' => $this->Boots->Database
                    ->term('admin_email')
                    ->get(),
                'transcript' => 'Dear [name],
The following is a transcript of the chat you had with our [department] support department.'
            ), 'strings' => array(
                'title'   => 'Awesome Live Chat',
                'status_online'  => 'We are online',
                'status_offline' => 'We are offline',
                'status_wait' => 'Connecting you with an operator',
                'status_start' => 'You are now chatting with [operator]',
                'field_name' => 'Name',
                'field_email' => 'Email',
                'field_website'=> 'Website',
                'field_dept'   => 'Department',
                'field_message'   => 'Message',
                'meta_visitor' => 'you',
                'meta_operator' => 'operator',
                'button_online' => 'Start Chat',
                'button_offline' => 'Send Message',
                'msg_welcome' => 'Hello [name], how may I help you.',
                'intro_online' => 'Fill in the form below so that one of our operators may help you out.',
                'intro_offline' => 'Fill in the form below to send us a message.'
            ), 'skin' => array(
                'font_header' => 'Open Sans',
                'font_body' => 'Open Sans',
                'color_primary_bg' => '#009edb',
                'color_primary_text' => '#ffffff',
                'color_secondary_bg' => '#393939',
                'color_secondary_text' => '#ffffff',
                'color_border' => '#b5b5b5',
                'border_transparency' => 20
            ), 'socket' => array(
                'socket'      => 'no',
                'host' => 'http://127.0.0.1',
                'port' => 3000
            ), 'css' => '', 'js' => ''
        );
    }

    /**
     * Run a custom select query.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global $wpdb
     * @access private
     * @param  string  Table name
     * @param  string  Fields csv
     * @param  string  Where clause
     * @param  array   Values for where clause placeholders
     * @param  bool    Return a single var or the results
     * @return mixed   Results as object, var, or null
     */
    private function cquery($table, $fields = '*', $where = '', $Escapes = array(), $single = false)
    {
        global $wpdb;
        $where_clause = $where ? ('WHERE ' . $where) : '';
        $sql = "SELECT $fields FROM $table $where_clause";
        $query = $where
            ? $wpdb->prepare($sql, $Escapes)
            : $sql;
        if($single)
            return $wpdb->get_var($query);
        else
            return $wpdb->get_results($query);
    }

    /**
     * Run a select query.
     * Joins where clauses with 'AND'.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::query()
     *         run a custom query
     * @access private
     * @param  string  Table name
     * @param  mixed   Fields array or single term
     * @param  array   Where clauses
     * @param  bool    Return a single var or the results
     * @return mixed   Results as object, var, or null
     */
    private function query($table, $Fields, $Where = array(), $single = false)
    {
        $f = is_array($Fields)
            ? implode(',', $Fields)
            : $Fields;
        $Escapes = array();
        $w = '';
        $sep = '';
        foreach($Where as $key => $val)
        {
            if(is_int($val)) $safe = '%d';
            else if(is_float($val)) $safe = '%f';
            else $safe = '%s';
            $Escapes[] = $val;
            $w .= $sep . $key . ' = ' . $safe;
            $sep = ' AND ';
        }
        return $this->cquery($table, $f, $w, $Escapes, $single);
    }

    /**
     * Update a table row.
     * Joins where clauses with 'AND'.
     *
     * @since  1.0.0
     * @uses   $wpdb->update() WordPress global $wpdb
     * @access private
     * @param  string  Table name
     * @param  array   Key value pairs to update
     * @param  array   Where clauses
     * @return void
     */
    private function update($table, $Updates, $Where)
    {
        global $wpdb;
        $Uph = array();
        foreach($Updates as $key => $val)
        {
            if(is_int($val)) $safe = '%d';
            else if(is_float($val)) $safe = '%f';
            else $safe = '%s';
            $Uph[] = $safe;
        }
        $Wph = array();
        foreach($Where as $key => $val)
        {
            if(is_int($val)) $safe = '%d';
            else if(is_float($val)) $safe = '%f';
            else $safe = '%s';
            $Wph[] = $safe;
        }
        $wpdb->update($table, $Updates, $Where, $Uph, $Wph);
    }

    /**
     * Query chat
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::cquery()
     *         run a custom query
     * @access private
     * @param  int    Visitor/Operator id
     * @param  mixed  Visitor status(s) to check against
     * @param  int    Offset by id
     * @param  bool   Visitor id or Operator id provided
     * @return mixed  Array of objects or null
     */
    private function queryChat($id, $Statuses = array(), $offset = 0, $isop = false)
    {
        if(!is_array($Statuses) && !empty($Statuses))
            $Statuses = array($Statuses);
        $Escapes = $Statuses;
        $where = '';
        $and   = '';
        if($Statuses)
        {
            $where .= 'u.status IN (';
            $where .= implode(',', array_fill(0, count($Statuses), '%s'));
            $where .= ')';
            $and = ' AND ';
        }
        if($offset)
        {
            $where .= $and . 'c.id > %d';
            $Escapes[] = $offset;
            $and = ' AND ';
        }
        $where .= $and;
        $where .= $isop ? 'c.operator_id' : 'c.user_id';
        $where .= ' = %d';
        $Escapes[] = $id;

        //$where .= $and;
        //$where .= 'u.id = c.user_id';

        $Chat = $this->cquery(
            $this->tbl_chat . ' c LEFT JOIN ' . $this->tbl_users . ' u on c.user_id = u.id',
            'c.id, c.guid, u.token, c.message, c.operator, c.seen, UNIX_TIMESTAMP(c.created_at) as time',
            $where,
            $Escapes
        );
        if(!$Chat) return $Chat;
        // time to UTC
        foreach($Chat as &$C)
        {
            $C->time = gmdate('Y-m-d H:i:s', $C->time);
        }
        return $Chat;
    }

    /**
     * Get a single field value.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::query()
     *         run a select query
     * @access private
     * @param  string Table name
     * @param  string Term to get
     * @param  string Key to check
     * @param  mixed  Value to check key against
     * @return mixed  Term value
     */
    private function get($table, $term, $key, $value)
    {
        $Where = array($key => $value);
        return $this->query($table, $term, $Where, true);
    }

    /**
     * Set a single field term to a value.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::update()
     *         run an update query
     * @access private
     * @param  string Table name
     * @param  string Term to get
     * @param  string Key to check
     * @param  mixed  Value to check key against
     * @param  mixed  Term value to update
     * @return void
     */
    private function set($table, $term, $key, $value, $newvalue)
    {
        $Update = array($term => $newvalue);
        $Where = array($key => $value);
        $this->update($table, $Update, $Where);
    }

    /**
     * Generate a token.
     * Check against table key
     * if sql provided.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global $wpdb
     * @access private
     * @param  int    Length of token
     * @param  string SQL to check against
     * @return string Token
     */
    private function token($length = 32, $sql = '')
    {
        if($sql) global $wpdb;
        $D = array_merge(range(0, 9), range('a', 'z'), range('A', 'Z'));
        do {
            $repeat = false;
            if(function_exists('open_random_pseudo_bytes'))
            {
                $token = bin2hex(openssl_random_pseudo_bytes(16, $strong));
                if(!$strong) $repeat = true;
            }
            else
            {
                $token = '';
                for ($i = 0; $i < $length; $i++)
                {
                    $token .= $D[array_rand($D)];
                }
            }
            if(!empty($sql) && !$repeat)
            {
                $sql = $wpdb->prepare($sql, $token);
                $id = $wpdb->get_var($sql);
                $repeat = $id != null ? true : false;
            }
        } while ($repeat);
        return $token;
    }

    /**
     * IP to country.
     *
     * @since  1.0.0
     * @uses   wp_remote_get() Remote HTTP GET
     * @access private
     * @param  string  IP address
     * @return array   Country details
     */
    private function ip2country($ip)
    {
        //http://freegeoip.net/json/46.19.37.108
        /*{
            "ip": "46.19.37.108",
            "country_code": "NL",
            "country_name": "Netherlands",
            "region_code": "",
            "region_name": "",
            "city": "",
            "zip_code": "",
            "time_zone": "Europe/Amsterdam",
            "latitude": 52.3667,
            "longitude": 4.9,
            "metro_code": 0
        }*/
        $url = $this->Options['IP2COUNTRY_URL'] . '/' . $ip;
        $raw_response = wp_remote_get($url, array(
            'user-agent' => 'WordPress/'. $this->Options['WP_VERSION'] .'; '. $this->Options['WP_SITE_URL']
        ));
        return (!is_wp_error($raw_response) && ($raw_response['response']['code'] == 200))
        ? json_decode($raw_response['body'], true)
        : array();
    }

    /**
     * Get country flag by country code.
     *
     * @since  1.0.0
     * @access public
     * @param  string  Country code.
     * @return string  URL to image flag.
     */
    public function getFlag($cc)
    {
        return !empty($cc)
        ? ($this->Options['APP_URL'] . '/images/flags/' . strtolower($cc) . '.png')
        : '';
    }

    public function pruneTable($where, $rows = 'expired')
    {
        switch($where)
        {
            case 'departments':
                $table = $this->tbl_departments;
                $where = 'disabled = 1';
            break;
            case 'visitors':
                $table = $this->tbl_users;
                $where = 'status = \'silent\' or (operator_id = \'0\' and status = \'done\')';
            break;
            case 'chat':
                $table = $this->tbl_chat;
                $where = 'NOT EXISTS(SELECT NULL FROM '.$this->tbl_users.' U
                    WHERE U.id = user_id)';
            break;
            default: return false;
        }
        if(!in_array($rows, array('all', 'expired'))) return false;
        global $wpdb;
        if($rows == 'expired')
        {
            $r = $wpdb->query("DELETE FROM $table WHERE $where");
            if($where != 'departments') return $r;
            $DepartmentsS = $this->getAllDepartments();
            if($DepartmentsS)
            {
                $Depts = array();
                foreach($DepartmentsS as $Dept)
                {
                    $Depts[] = $Dept->id;
                }
                $Departments = $this->Boots->Database
                    ->term('alc_departments', array())
                    ->get();
                $DepartmentsN = $Departments;
                foreach($Departments as $key => $time)
                {
                    preg_match('/_([0-9]*)/', $key, $Match);
                    $did = $Match[1];
                    if(!in_array($did, $Depts))
                        unset($DepartmentsN[$key]);
                }
                $this->Boots->Database
                ->term('alc_departments')
                ->update($DepartmentsN);
            }
            else $this->Boots->Database
                ->term('alc_departments')
                ->update(array());
            return $r;
        }
        else if($rows == 'all')
        {
            $r = $wpdb->query("TRUNCATE TABLE $table");
            if($where != 'departments') return $r;
            $this->Boots->Database
                ->term('alc_departments')
                ->update(array());
            return $r;
        }
        return false;
    }

    /**
     * Get default settings.
     *
     * @since  1.0.0
     * @access public
     * @param  string Setting category
     * @param  string Setting term
     * @return mixed
     */
    public function defaults($category = null, $term = null)
    {
        $Settings = self::$Default_Settings;
        if(is_array($category))
        {
            foreach($category as $c => $terms)
            {
                if(is_array($terms))
                {
                    foreach($terms as $t)
                    {
                        $Settings[$c][$t] = $this->settings($c, $t);
                    }
                }
                else $Settings[$c][$terms] = $this->settings($c, $terms);
            }
        }
        else if($category)
        {
            if(!isset($Settings[$category]))
                return null;
            if($term) return isset($Settings[$category][$term])
                ? $Settings[$category][$term]
                : null;
            return $Settings[$category];
        }
        return $Settings;
    }

    /**
     * HTTP => HTTPS
     * helper
     *
     * @since  1.2.0
     * @access public
     * @param  mixed  Value
     * @param  string Key
     * @return mixed
     */
    public function sslizer($value, $key)
    {
        $value = str_replace('http://', 'https://', $value);
    }

    /**
     * HTTP => HTTPS
     *
     * @since  1.2.0
     * @uses   AwesomeLiveChat_Library::sslizer()
     *         Helper for sslizing a string
     * @access public
     * @param  mixed  Data
     * @return mixed
     */
    public function sslize($mixed)
    {
        if(!is_ssl()) return $mixed;
        if(!is_array($mixed))
            return str_replace('http://', 'https://', $mixed);
        array_walk_recursive($mixed, array($this, 'sslizer'));
        return $mixed;
    }

    /**
     * Get plugin settings.
     *
     * @since  1.2.0
     * Allow for http to https if ssl
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::defaults()
     *         get default settings
     * @access public
     * @param  string Setting category
     * @param  string Setting term
     * @param  bool   Return default if value empty?
     * @return mixed
     */
    public function settings($category = null, $term = null, $default = true)
    {
        //if($category == 'general' && $term == 'icon')
        //    die()
        $Settings = self::$Settings;
        if($category)
        {
            if(!isset($Settings[$category]))
                return $this->sslize($this->defaults($category, $term));
            if($term) return $this->sslize(isset($Settings[$category][$term])
                ? ($default && !$Settings[$category][$term]
                    ? $this->defaults($category, $term)
                    : $Settings[$category][$term])
                : $this->defaults($category, $term));
            return $this->sslize($default && !$Settings[$category]
                ? $this->defaults($category, $term)
                : $Settings[$category]);
            //return $Settings[$category];
        }
        return $this->sslize($default && !$Settings
            ? $this->defaults()
            : $Settings);
    }

    /**
     * Freshup settings
     *
     * @since  1.0.0
     * @access public
     * @param  void
     * @return array  Fresh settings from database
     */
    public function touchSettings()
    {
        global $wpdb;
        self::$Settings = unserialize($this->get(
            $wpdb->options,
            'option_value',
            'option_name',
            'alc_settings'
        ));
        return self::$Settings;
    }

    /**
     * Save/Override default settings.
     * Runs when plugin is
     * installed/activated.
     *
     * @since  1.2.0
     * Include new defaults with merge
     *
     * @since  1.0.0
     * @uses   Boots::Database Save options
     * @access public
     * @param  string Plugin version
     * @return void
     */
    public function factory($installed_version)
    {
        $this_version = $this->Options['APP_VERSION'];
        if($installed_version && version_compare($installed_version, $this_version, '>='))
            return false;
        if(!$installed_version)
        {
            // Add a default department.
            $this->addDepartments(array(__('Support', 'awesome-live-chat')));
            // Assign the department to the current admin.
            $User = wp_get_current_user();
            $User->add_cap('operate_alc');
            $this->setOperator($User->ID, 'alc_departments', array(1));
            $this->setOperator($User->ID, 'alc_token', $this->generateOperatorToken());
        }
        $this->Boots->Database
        ->term('alc_settings')
        ->update($this->Utility->join(
            $this->defaults(),
            $this->settings()
        ));
    }

    /**
     * Generate a gravatar.
     *
     * @since  1.0.0
     * @access public
     * @param  string  Email
     * @param  int     Size
     * @return string  Url to gravatar
     */
    public function gravatar($email, $size = 45)
    {
        return 'http'
                . 's'//(is_ssl() ? 's' : '')
                . '://www.gravatar.com/avatar/'
                . md5(strtolower($email))
                . '?s=' . $size
                . "&d=mm";
    }

    /**
     * Generate a gravatar
     * for an operator.
     *
     * @since  1.0.0
     * @uses   get_userdata()
     *         get wp user data
     * @uses   AwesomeLiveChat_Library::gravatar()
     *         get gravatar by email
     * @access public
     * @param  int     Operator id
     * @param  string  Operator email
     * @param  int     Size
     * @return string  Url to gravatar
     */
    public function gravatarOperator($id = false, $email = '', $size = 45)
    {
        $gravatar = $this->settings('general', 'avatar');
        if(!empty($gravatar)) return $this->Boots->Media
            ->image($gravatar)->width(45)->height(45)->get();
        if($email) return $this->gravatar($email);
        if(!$id) return '';
        $Operator = get_userdata($id);
        return $this->gravatar($Operator->user_email);
    }

    /**
     * Online/Offline check.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::settings()
     *         get app settings
     * @uses   AwesomeLiveChat_Library::getAllDepartments()
     *         get chat departments
     * @access public
     * @param  int    Department id
     * @return bool   Online?
     */
    public function isOnline($department = null)
    {
        if($this->settings('general', 'mode', true) != 'online')
            return false;
        // mode is online, proceed for departments
        $now = time();
        $Departments = $this->Boots->Database
            ->term('alc_departments', array())
            ->get();
        // any enabled department
        if($department == null)
        {
            foreach($this->getAllDepartments(true) as $Dept)
            {
                if(isset($Departments['_' . $Dept->id]))
                {
                    $time = $Departments['_' . $Dept->id];
                    $diff = $time - $now;
                    $minutes = ($diff/60);
                    $online = $minutes > -2; # TODO, allow adjustment
                    if($online) return true;
                }
            }
            return false;
        }
        // single department
        if(!isset($Departments['_' . $department])) return false;
        $time = $Departments['_' . $department];
        $diff = $time - $now;
        $minutes = ($diff/60);
        return $minutes > -2; # TODO, allow adjustment
    }

    /**
     * Get all chat departments
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::query()
     *         run a select query
     * @access public
     * @param  bool   Only enabled departments?
     * @return object Chat departments
     */
    public function getAllDepartments($enabled = false)
    {
        $Where = !$enabled ? array() : array('disabled' => 0);
        return $this->query($this->tbl_departments, '*', $Where);
    }

    /**
     * Add or activate departments.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global wpdb object
     * @access public
     * @param  array  Name of departments
     * @return void
     */
    public function addDepartments($Names)
    {
        if(!$Names) return false;
        global $wpdb;
        $time = current_time('mysql');
        $Values = array_fill(0, count($Names), "(%s,'$time','$time')");
        $values = implode(',', $Values);
        $table = $this->tbl_departments;
        $query = "INSERT INTO $table
                (name,created_at,updated_at) VALUES $values
                ON DUPLICATE KEY UPDATE disabled = 0";
        $sql = $wpdb->prepare($query, $Names);
        $wpdb->query($sql);
    }

    /**
     * Disable departments.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global wpdb object
     * @access public
     * @param  array  Name of departments
     * @return void
     */
    public function disableDepartments($Names)
    {
        if(!$Names) return false;
        global $wpdb;
        $time = current_time('mysql');
        $Values = array_fill(0, count($Names), '%s');
        $values = implode(',', $Values);
        $table = $this->tbl_departments;
        $query = "UPDATE $table SET disabled = 1, updated_at = '$time'
                WHERE name IN ($values)";
        $sql = $wpdb->prepare($query, $Names);
        $wpdb->query($sql);
    }

    /**
     * Get a department term value.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::get()
     *         get a single term value
     * @access public
     * @param  string Term to get
     * @param  string Key to check
     * @param  mixed  Value to check key against
     * @return mixed  Department term value or null
     */
    public function getDepartment($term, $key, $val)
    {
        return $this->get($this->tbl_departments, $term, $key, $val);
    }

    /**
     * Set a department term key to a value.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::set()
     *         set a single term key to a value
     * @access public
     * @param  string Term to set
     * @param  string Key to check
     * @param  mixed  Value to check key against
     * @param  mixed  New term value to set
     * @return void
     */
    public function setDepartment($term, $key, $val, $new)
    {
        $this->set($this->tbl_departments, $term, $key, $val, $new);
    }

    /**
     * Generate a visitor/user token.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::token()
     *         generate token
     * @access public
     * @param  void
     * @return string Token
     */
    public function generateVisitorToken()
    {
        $table = $this->tbl_users;
        return $this->token(32, "SELECT id FROM $table WHERE token = %s");
    }

    /**
     * Create a visitor/user.
     * Make a pulse.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global $wpdb
     * @uses   AwesomeLiveChat_Library::generateVisitorToken()
     *         generate visitor token
     * @uses   AwesomeLiveChat_Library::gravatar()
     *         generate gravatar
     * @access public
     * @param  string Visitor name
     * @param  string Visitor email
     * @param  int    Visitor department
     * @param  string Visitor website
     * @param  string Visitor reference url
     * @return array  Visitor data
     */
    public function createVisitor($name, $email, $department, $website, $uri)
    {
        global $wpdb;
        $ip = $_SERVER["REMOTE_ADDR"];
        $Geo = $this->ip2country($ip);
        $time = current_time('mysql');
        $Visitor = array_merge(array(
            'token'      => (string) $this->generateVisitorToken(),
            'status'     => 'waiting',
            'gravatar'   => (string) $this->gravatar($email),
            'ip'         => $ip,
            'country'    => isset($Geo['country_name'])
                            ? $Geo['country_name'] : '',
            'city'       => isset($Geo['city'])
                            ? $Geo['city'] : '',
            'cc'         => isset($Geo['country_code'])
                            ? $Geo['country_code'] : '',
            'ping_at'    => $time,
            'created_at' => $time
        ), compact('name', 'email', 'department', 'website', 'uri'));
        $wpdb->insert($this->tbl_users, $Visitor, array(
            '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s',
            '%s', '%s', '%d', '%s', '%s'
        ));
        unset($Visitor['ip'], $Visitor['created_at']);
        return $Visitor;
    }

    public function getAVisitor($token)
    {
        $Visitor = $this->query(
            $this->tbl_users,
            array('id', 'token', 'status', 'name', 'email', 'department',
            'website', 'gravatar', 'ip', 'uri', 'city', 'country', 'cc'),
            array('token' => (string) $token),
            false
        );
        $Return = $Visitor ? array_shift($Visitor) : null;
        if($Return) $Return->flag = $this->getFlag($Return->cc);
        return $Return;
    }

    /**
     * Get a visitor term value.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::get()
     *         get a single term value
     * @access public
     * @param  string Term to get
     * @param  string Key to check
     * @param  mixed  Value to check key against
     * @return mixed  Visitor term value or null
     */
    public function getVisitor($term, $key, $val)
    {
        return $this->get($this->tbl_users, $term, $key, $val);
    }

    /**
     * Set a visitor term key to a value.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::set()
     *         set a single term key to a value
     * @access public
     * @param  string Term to set
     * @param  string Key to check
     * @param  mixed  Value to check key against
     * @param  mixed  New term value to set
     * @return void
     */
    public function setVisitor($term, $key, $val, $new)
    {
        $this->set($this->tbl_users, $term, $key, $val, $new);
    }

    /**
     * Ping a visitor.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::setVisitor()
     *         set a visitor's term value
     * @access public
     * @param  id     Visitor id
     * @return void
     */
    public function visitorPing($id)
    {
        $this->setVisitor('ping_at', 'id', (int) $id, current_time('mysql'));
        return true;
    }

    /**
     * Check to see if visitor is online.
     * Update status to done if offline.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::getVisitor()
     *         get a visitor's term value
     * @uses   AwesomeLiveChat_Library::setVisitor()
     *         set a visitor's term value
     * @access public
     * @param  id     Visitor id
     * @param  string Datetime of ping
     * @return bool   Is online?
     */
    public function isVisitorOnline($id, $status = null, $time = null)
    {
        if(!$status)
            $status = $this->getVisitor('status', 'id', $id);
        if(!$time)
            $time = $this->getVisitor('ping_at', 'id', $id);
        $time = strtotime($time);
        $now = strtotime(current_time('mysql'));
        $diff = $time - $now;
        $days = ($diff/86400);
        $hours = ($diff/3600);
        $minutes = ($diff/60);
        $seconds = ($diff);
        $online = $minutes > -2; # TODO, allow adjustment
        if(!$online && ($status == 'waiting' || $status == 'busy'))
            $this->setVisitor('status', 'id', $id, 'done');
        return $online;
    }

    /**
     * Get visitor id by token.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::getVisitor()
     *         get a visitor's term value
     * @access public
     * @param  int     Visitor id
     * @return mixed   Token or null
     */
    public function visitorID($token)
    {
        return $this->getVisitor('id', 'token', (string) $token);
    }

    /**
     * Get visitor token by id.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::getVisitor()
     *         get a visitor's term value
     * @access public
     * @param  int     Visitor id
     * @return mixed   Token or null
     */
    public function visitorToken($id)
    {
        return $this->getVisitor('token', 'id', (int) $id);
    }

    /**
     * Get visitors tokens by ids.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::getVisitor()
     *         get a visitor's term value
     * @access public
     * @param  int     Visitor id
     * @return mixed   Token or null
     */
    public function visitorsTokens($Ids = array())
    {
        if(!$Ids) return array();
        $where = 'id IN (';
        $where .= implode(',', array_fill(0, count($Ids), '%d'));
        $where .= ')';
        $Tokens = $this->cquery(
            $this->tbl_users,
            'token',
            $where,
            $Ids,
            false
        );
        $Return = array();
        foreach($Tokens as $Token) {
            $Return[] = $Token->token;
        }
        return $Return;
    }

    /**
     * Get a visitor's operator
     * by id and status.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::query()
     *         run a select query
     * @access public
     * @param  int    Visitor id
     * @param  string Visitor status
     * @return int    Operator id or 0
     */
    public function visitorOperator($id, $status = '')
    {
        $Where = array('id' => (int) $id);
        if(!empty($status))
            $Where['status'] = (string) $status;
        return $this->query(
            $this->tbl_users,
            'operator_id',
            $Where,
            true
        );
    }

    /**
     * Get visitor's chat
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::queryChat()
     *         run a chat query
     * @access public
     * @param  int    Visitor id
     * @param  mixed  Visitor status(s) to check against
     * @param  int    Offset by id
     * @return mixed  Array of objects or null
     */
    public function visitorChat($id, $Statuses = array(), $offset = 0)
    {
        $Chat = $this->queryChat($id, $Statuses, $offset, false);
        /*$this->update($this->tbl_chat, array(
                'guid' => ''
            ), array(
                'user_id'  => (int) $id,
                'operator' => 0
        ));*/
        return $Chat;
    }

    /**
     * Generate an operator token.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global $wpdb
     * @uses   AwesomeLiveChat_Library::token()
     *         generate token
     * @access public
     * @param  void
     * @return string Token
     */
    public function generateOperatorToken()
    {
        global $wpdb;
        $table = $wpdb->prefix . 'usermeta';
        return self::token(32,
            "SELECT user_id FROM $table
             WHERE meta_key = 'alc_token' AND meta_value = %s"
        );
    }

    /**
     * Get an operator term value.
     *
     * @since  1.0.0
     * @uses   get_user_meta() Return a user meta
     * @access public
     * @param  int    Operator id
     * @param  string Term key
     * @return mixed  Term value
     */
    public function getOperator($id, $term)
    {
        return get_user_meta($id, $term, true);
    }

    /**
     * Set an operator term key to a value.
     *
     * @since  1.0.0
     * @uses   update_user_meta() Update a user meta
     * @access public
     * @param  int    Operator id
     * @param  string Term key
     * @param  mixed  Term value
     * @return void
     */
    public function setOperator($id, $term, $value)
    {
        return update_user_meta($id, $term, $value);
    }

    /**
     * Get all operator ids
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global $wpdb
     * @uses   AwesomeLiveChat_Library::query()
     *         run a select query
     * @access public
     * @param  void
     * @return mixed  ids or null
     */
    public function getAllOperators()
    {
        global $wpdb;
        $table = $wpdb->prefix . 'usermeta';
        return $this->query($table, 'user_id', array(
            'meta_key' => 'alc_departments'
        ));
    }

    /**
     * Does operator belong to a department.
     *
     * @since  1.0.0
     * @uses   get_current_user_id()
     *         get the current operator's id
     * @uses   AwesomeLiveChat_Library::getOperator()
     *         get an operator's term
     * @access public
     * @param  int    Operator id
     * @return void
     */
    public function operatorHasDepartments($id = 0)
    {
        $id = $id ? $id : get_current_user_id();
        $Departments = $this->getOperator($id, 'alc_departments');
        if($Departments && is_array($Departments))
            return true;
        return false;
    }

    /**
     * Get operator id by token.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global $wpdb
     * @uses   AwesomeLiveChat_Library::query()
     *         run a select query
     * @access public
     * @param  string  Operator token
     * @return mixed   id or null
     */
    public function operatorID($token)
    {
        global $wpdb;
        $table = $wpdb->prefix . 'usermeta';
        return $this->query($table, 'user_id', array(
            'meta_key' => 'alc_token',
            'meta_value' => (string) $token
        ), true);
    }

    /**
     * Get operator token by id.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global $wpdb
     * @uses   AwesomeLiveChat_Library::query()
     *         run a select query
     * @access public
     * @param  int     Operator id
     * @return mixed   Token or null
     */
    public function operatorToken($id)
    {
        global $wpdb;
        $table = $wpdb->prefix . 'usermeta';
        return $this->query($table, 'meta_value', array(
            'meta_key' => 'alc_token',
            'user_id'  => (int) $id
        ), true);
    }

    /**
     * Authenticate an operator
     * by username and password
     * or if already logged in.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global $wpdb
     * @uses   is_user_logged_in()    Is user logged in?
     * @uses   current_user_can()     Current operator permission
     * @uses   get_current_user_id()  Get current operator id
     * @uses   user_can()             Operator permission
     * @uses   AwesomeLiveChat_Library::query()
     *         run a select query
     * @access public
     * @param  string  Operator username
     * @param  string  Operator password/pincode
     * @return mixed   Operator id, null or -1 (no permission)
     */
    public function authenticateOperator($username, $password)
    {
        if(is_user_logged_in() && current_user_can('operate_alc'))
            return get_current_user_id();
        global $wpdb;
        $users    = $wpdb->prefix . 'users';
        $usermeta = $wpdb->prefix . 'usermeta';
        $id = $this->query(
            $users . ' a LEFT JOIN ' . $usermeta  . ' b on a.ID = b.user_id',
            'a.ID',
            array(
                'b.meta_key'   => 'alc_pincode',
                'a.user_login' => (string) $username,
                'b.meta_value' => md5($password)
        ), true);

        return $id
            ? (user_can($id, 'operate_alc') ? $id : -1)
            : null;
    }

    /**
     * Ping operator and their departments.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::getOperator()
     *         get an operators's term
     * @access public
     * @param  int    Operator id
     * @return bool   Pinged?
     */
    public function operatorPing($id)
    {
        $now = time();
        // Ping operator
        $Operators = $this->Boots->Database
            ->term('alc_users', array())
            ->get();
        $Operators['_' . $id] = $now;
        $this->Boots->Database
            ->term('alc_users')
            ->update($Operators);
        // Ping departments for offline vs online trigger
        $Dids = $this->getOperator($id, 'alc_departments');
        if(!is_array($Dids)) return false;
        $Departments = $this->Boots->Database
            ->term('alc_departments', array())
            ->get();
        foreach($Dids as $did)
        {
            $Departments['_' . $did] = $now;
        }
        $this->Boots->Database
            ->term('alc_departments')
            ->update($Departments);
        return true;
    }

    /**
     * Is operator online?
     *
     * @since  1.0.0
     *
     * @access public
     * @param  int    Operator id
     * @return bool   Online?
     */
    public function isOperatorOnline($id)
    {
        if(!$id) return false;
        if($this->settings('general', 'mode') != 'online')
            return false;
        // mode is online, proceed for operator last ping
        $now = time();
        global $wpdb;
        $Operators = unserialize($this->get(
            $wpdb->options,
            'option_value',
            'option_name',
            'alc_users'
        ));
        if(!$Operators || !isset($Operators['_' . $id])) return false;
        $time = $Operators['_' . $id];
        $diff = $time - $now;
        $minutes = ($diff/60);
        return $minutes > -2; # TODO, allow adjustment
    }

    /**
     * Query operator visitors
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::cquery()
     *         run a custom query
     * @uses   AwesomeLiveChat_Library::getOperator()
     *         get an operator's term
     * @access public
     * @param  int    Operator id (0 for waiting visitors only)
     * @param  mixed  Visitor status(s) to check against
     * @param  int    Offset by id
     * @return mixed  Array of objects or null
     */
    public function operatorVisitors($id, $Statuses = array(), $offset = 0)
    {
        if(!is_array($Statuses) && !empty($Statuses))
            $Statuses = array($Statuses);
        $Escapes = $Statuses;
        $where = '';
        $and   = '';
        if($Statuses)
        {
            $where .= 'status IN (';
            $where .= implode(',', array_fill(0, count($Statuses), '%s'));
            $where .= ')';
            $and = ' AND ';
        }
        if($offset)
        {
            $where .= $and . 'u.id > %d';
            $Escapes[] = $offset;
            $and = ' AND ';
        }

        $Departments = $this->getOperator($id, 'alc_departments');
        if(!$Departments) $Departments = array(0);
        $Escapes = array_merge($Escapes, $Departments);
        $where .= $and;
        $where .= '(((department IN (';
        $where .= implode(',', array_fill(0, count($Departments), '%d'));
        $where .= ') AND operator_id = %d) OR operator_id = %d)';
        $Escapes[] = $id;
        $Escapes[] = $id;
        $where .= ' OR ';
        $where .= '(operator_id = 0 AND status = "waiting")';
        $where .= ' AND department IN (';
        $Escapes = array_merge($Escapes, $Departments);
        $where .= implode(',', array_fill(0, count($Departments), '%d'));
        $where .= ')';
        $where .= ')';

        $Results = $this->cquery(
            $this->tbl_users,
            'id, token, status, name, email,department,
            website, gravatar, uri, ip, country, city, cc, ping_at as ping',
            $where,
            $Escapes
        );
        return $Results ? $Results : array();
    }

    /**
     * Get operator's welcome message.
     *
     * @since  1.0.0
     * @uses   get_current_user_id()
     *         get the current operator's id
     * @uses   AwesomeLiveChat_Library::getOperator()
     *         get an operators's term
     * @access public
     * @param  int    Operator id
     * @return string Welcome message
     */
    public function getOperatorWelcomeMessage($id = 0)
    {
        $id = $id ? $id : get_current_user_id();
        $welcome = $this->getOperator($id, 'alc_welcome');
        return $welcome
            ? $welcome
            : $this->settings('strings', 'msg_welcome');
    }

    /**
     * Welcome a visitor.
     * Post welcome message.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::update()
     *         run an update query
     * @uses   AwesomeLiveChat_Library::postChat()
     *         post a message
     * @uses   AwesomeLiveChat_Library::getVisitor()
     *         get a visitor's term
     * @uses   AwesomeLiveChat_Library::getWelcomeMessage()
     *         get a operator's welcome message
     * @access public
     * @param  int    Operator id
     * @param  int    Visitor id
     * @param  token  Visitor token
     * @return array  Welcome chat message
     */
    public function operatorWelcome($operator_id, $user_id, $user_token = false)
    {
        $this->update($this->tbl_users, array(
            'operator_id' => (int) $operator_id,
            'status'      => 'busy'
        ), array(
            'id' => (int) $user_id,
            'operator_id' => 0,
            'status' => 'waiting'
        ));
        # get operator's welcome message
        $message = $this->getOperatorWelcomeMessage($operator_id);
        $message = str_replace(
            '[name]',
            $this->getVisitor('name', 'id', (int) $user_id),
            $message
        );
        $id = $this->postChat($user_id, $operator_id, $message, 1);
        return array(
            'id'    => $id,
            'token' => $user_token
                ? $user_token
                : $this->getVisitor('token', 'id', (int) $user_id),
            'message' => $message,
            'operator' => 1
        );
    }

    /**
     * See a visitor.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::update()
     *         run an update query
     * @access public
     * @param  int    Operator id
     * @param  int    Visitor id
     * @return void
     */
    public function operatorSaw($operator_id, $user_id)
    {
        $this->update($this->tbl_chat, array(
            'seen' => 1
        ), array(
            'user_id' => (int) $user_id,
            'operator_id' => (int) $operator_id
        ));
    }

    /**
     * Get operator's chat
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::queryChat()
     *         run a chat query
     * @access public
     * @param  int    Operator id
     * @param  mixed  Visitor status(s) to check against
     * @param  int    Offset by id
     * @return mixed  Array of objects or null
     */
    public function operatorChat($id, $Statuses = array(), $offset = 0)
    {
        $Chat = $this->queryChat($id, $Statuses, $offset, true);
        /*$this->update($this->tbl_chat, array(
                'guid' => ''
            ), array(
                'operator_id' => (int) $id,
                'operator'    => 1
        ));*/
        return $Chat;
    }

    /**
     * Get operator archives.
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::operatorVisitors()
     *         Get operator visitors
     * @uses   AwesomeLiveChat_Library::operatorChat()
     *         Get operator chat
     * @access public
     * @param  int    Operator id
     * @return array  Archives
     */
    public function operatorArchives($id = 0)
    {
        $id = $id ? $id : get_current_user_id();
        $Archives = array();
        $Visitors = $this->operatorVisitors($id, array('archive'));
        foreach($Visitors as $Visitor)
        {
            $Archives[$Visitor->token] = new stdClass;
            $Archives[$Visitor->token] = $Visitor;
        }
        $Chat = $this->operatorChat($id, array('archive'));
        foreach($Chat as $Message)
        {
            if(isset($Archives[$Message->token]))
                $Archives[$Message->token]->chat[] = $Message;
        }
        return $Archives;
    }

    /**
     * Save a chat message.
     *
     * @since  1.0.0
     * @uses   $wpdb  WordPress global $wpdb
     * @access public
     * @param  int    User id
     * @param  int    Operator id
     * @param  string Message
     * @param  int    Is operator?
     * @param  string Backbonejs id
     * @return int    Inserted id
     */
    public function postChat($user_id, $operator_id, $message, $operator = 0, $guid = '')
    {
        global $wpdb;
        $wpdb->insert($this->tbl_chat, array_merge(array(
            'created_at' => (string) current_time('mysql')
        ), compact('user_id', 'operator_id', 'message', 'operator', 'guid')), array(
            '%s', '%d', '%d', '%s', '%d', '%s'
        ));
        return $wpdb->insert_id;
    }

    /**
     * Send email - offline message.
     *
     * @filter do_action awesome_live_chat_email_headers
     * @filter do_action awesome_live_chat_email_offline
     *
     * @since  1.0.0
     * @uses   AwesomeLiveChat_Library::getDepartment()
     *         get department term
     * @access public
     * @param  string Visitor name
     * @param  string Visitor email
     * @param  int    Visitor department
     * @param  string Visitor website
     * @param  string Visitor reference url
     * @param  string Message
     * @return bool   Mail sent?
     */
    public function sendOfflineMessage($name, $email, $department, $website, $uri, $message)
    {
        $name = strip_tags($name);
        $email = strip_tags($email);
        $department = $this->getDepartment('name', 'id', $department);
        $website = strip_tags($website);
        $uri = strip_tags($uri);
        $message = strip_tags($message);

        $subject = $this->settings('mail', 'offline_subject');
        $receiver = $this->settings('mail', 'offline_receiver_email');
        $sender_name = $this->settings('mail', 'offline_sender_name');
        $sender_email = $this->settings('mail', 'offline_sender_email');
        if(empty($sender_email))
        {
            $sender_name = $name;
            $sender_email = $email;
        }
        $Headers = array(
            'From: ' . ($sender_name . ' <' . $sender_email . '>'),
            'Reply-to: ' . ($name . ' <' . $email . '>')
        );
        $Headers = apply_filters('awesome_live_chat_email_headers', $Headers, 'offline');

        $body = __('Name', 'awesome-live-chat') . ':          ' . $name . "\r\n"
              . __('Email', 'awesome-live-chat') . ':           ' . $email . "\r\n"
              . __('Website', 'awesome-live-chat') . ':       ' . $website . "\r\n"
              . __('URI', 'awesome-live-chat') . ':              ' . $uri . "\r\n"
              . __('Department', 'awesome-live-chat') . ': ' . $department . "\r\n"
              . "\r\n"
              . __('Message', 'awesome-live-chat') . ':    ' . "\r\n"
              . '--------------' . "\r\n"
              . $message;

        $body = wordwrap(apply_filters('awesome_live_chat_email_offline', $body,
            $name, $email, $website, $department, $uri, $message
        ), 70, "\r\n");

        return wp_mail($receiver, $subject, $body, $Headers);
    }

    public function sendTranscript($id)
    {
        $Chat = $this->queryChat($id);
        if(!$Chat) return false;

        $subject = $this->settings('mail', 'transcript_subject');
        $sender_name = $this->settings('mail', 'transcript_sender_name');
        $sender_email = $this->settings('mail', 'transcript_sender_email');
        $transcript = $this->settings('mail', 'transcript');

        $Headers = array('From: ' . ($sender_name . ' <' . $sender_email . '>'));
        $Headers = apply_filters('awesome_live_chat_email_headers', $Headers, 'transcript');

        $Visitor = $this->getAVisitor($this->visitorToken($id));

        $transcript = preg_replace(
            array('/\[name\]/i', '/\[email\]/i', '/\[department\]/i'),
            array(
                $Visitor->name,
                $Visitor->email,
                $this->getDepartment('name', 'id', $Visitor->department),

            ),
            $transcript
        ) . "\r\n\r\n";

        $oid = $this->visitorOperator($id);
        $Operator = get_userdata($oid);
        $oname = $Operator->display_name;

        foreach($Chat as $Message)
        {
            $transcript .= '[' . $Message->time . '] ' . "\r\n";
            $transcript .= ' - ' . ((bool) $Message->operator ? $oname : $Visitor->name);
            $transcript .= ': ' . $Message->message;
            $transcript .= "\r\n";
        }

        $transcript = wordwrap(apply_filters('awesome_live_chat_email_transcript', $transcript,
            $id, $oid, $Chat
        ), 70, "\r\n");

        return wp_mail($Visitor->email, $subject, $transcript, $Headers);
    }
}
