<?php
// vim: ts=2 sw=2

  include_once "base_controller.php";
  include_once "data_formatters.php";
  // include_once "elapsed_time.php";

  class DataExportController extends BaseController {
    var $special_keys = array('table', 'columns', 'dt_after', 'dt_before', 'dt_start', 'dt_end', 'dt_tz', 'dt_period', 'dt_week_start_on_sunday', 'format', 'file', 'true_text', 'false_text', 'null_text', 'last_n_row', 'save_to_local', 'zip_threshold', 'auth_key');

    protected function signinRequired() {
      return true;
    }

    protected function authKeyRequired() {
      return true;
    }

    protected function sameHostAuthFree() {
      return true;
    }

    protected function tz() {
      $dt_tz = null;
      if (isset($_GET['dt_tz']))
        $dt_tz = $_GET['dt_tz'];
      try {
        $result = new DateTimeZone($dt_tz);
        return $result;
      } catch(Exception $e) {
        $error = L('"dt_tz" is not valid: ') . $dt_tz;
        error_log($error);
        // $default_tz = date_default_timezone_get();
        $default_tz = 'UTC';
        try {
          $result = new DateTimeZone($default_tz);
          return $result;
        } catch(Exception $e) {
          $error = 'default timezone "' . $default_tz .'" is not valid';
          error_log($error);
          $this->renderErrorAndHelp($error);
        }
      }
    }

    protected function convertTS($paramName, $tz) {
      $timeString = array_key_exists($paramName, $_GET) ? $_GET[$paramName] : null;
      $date = NULL;
      if (empty($timeString))
        return $date;

      $date = DateTime::createFromFormat('YmdHis', $timeString, $tz);
      if (empty($date))
          $this->renderErrorAndHelp(L('wrong datetime format: ') . $timeString);
      $date->setTimezone(new DateTimeZone('UTC'));
      return $date;
    }
    
    protected function tzOffset($userTZ) {
      $utcTZ = new DateTimeZone("UTC");
      $utcTime = new DateTime('now', $utcTZ);
      return $userTZ->getOffset($utcTime)/3600;
    }

    protected function processDateTimePeriod($dt_tz, &$dt_after, &$dt_before) {
      $dt_period_str = array_key_exists('dt_period', $_GET) ? $_GET['dt_period'] : null;
      if (empty($dt_period_str))
        return;

      $week_start_on_sunday = false;
      $start_on_sunday = $_GET['dt_week_start_on_sunday'];
      $week_start_on_sunday = (isset($start_on_sunday) && ($start_on_sunday == '1' || $start_on_sunday == 'true'));

      $now = new DateTime('now', $dt_tz);
      $today_no = $now->format('N');
      $after_str = null;
      $before_str = null;
      if (strcasecmp($dt_period_str, 'today') == 0) {
        $after_str = 'today';
        $before_str = 'tomorrow';
      } else if (strcasecmp($dt_period_str, 'yesterday') == 0) {
        $after_str = 'yesterday';
        $before_str = 'today';
      } else if (strcasecmp($dt_period_str, 'last_24hr') == 0) {
        $after_str = '-24 hours';
        $before_str = 'now';
      } else if (strcasecmp($dt_period_str, 'week_to_date') == 0) {
        if ($week_start_on_sunday) {
          if ($today_no == 7) // if today is Sunday
            $after_str = 'today';
          else
            $after_str = 'Sunday last week';
        }
        else {
          //GOTCHA: 'Monday this week' works well except on Sunday, it will return the Monday of next week on Sunday. Following logics fix this issue.
          $after_str = '-' . ($today_no-1) . ' days today';
        }
        $before_str = 'tomorrow';
      } else if (strcasecmp($dt_period_str, 'last_week') == 0) {
        if ($week_start_on_sunday) {
          $before_str = 'Sunday last week';
          if ($today_no == 7 )
            $before_str = 'today';

          $after_str = '-7 days ' . $before_str;
        } else {
          $after_str = '-' . ($today_no+6) . ' days today';
          $before_str = '-' . ($today_no-1) . ' days today';
        }
      } else if (strcasecmp($dt_period_str, 'month_to_date') == 0) {
        $after_str = 'midnight first day of this month';
        $before_str = 'tomorrow';
      } else if (strcasecmp($dt_period_str, 'last_month') == 0) {
        $after_str = 'midnight first day of last month';
        $before_str = 'midnight first day of this month';
      } else if (strcasecmp($dt_period_str, 'year_to_date') == 0) {
        $after_str = 'midnight first day of January this year';
        $before_str = 'tomorrow';
      } else if (strcasecmp($dt_period_str, 'last_year') == 0) {
        $after_str = 'midnight first day of January last year';
        $before_str = 'midnight first day of January this year';
      } else {
        $err_msg = $dt_period_str . L(' is not a valid date period value');
        error_log($err_msg);
        $this->renderErrorAndHelp($err_msg);
        return;
      }

      $utc_tz = new DateTimeZone('UTC');
      if (!is_null($after_str))
      {
        $dt_after = new DateTime($after_str, $dt_tz);
        $dt_after->setTimezone($utc_tz);
      }
      if (!is_null($before_str))
      {
        $dt_before = new DateTime($before_str, $dt_tz);
        $dt_before->setTimezone($utc_tz);
      }
    }

    protected function doAjaxGet() {
      $this->doGet();
    }

    protected function doGet() {
      $table = $_GET['table'];
      $columns = array_key_exists('columns', $_GET) ? $_GET['columns'] : null; 

      $dt_tz = $this->tz();

      $dt_start_param_name = 'dt_after';
      if (array_key_exists('dt_start', $_GET))
        $dt_start_param_name = 'dt_start';
      $dt_after = $this->convertTS($dt_start_param_name, $dt_tz);

      $dt_end_param_name = 'dt_before';
      if (array_key_exists('dt_end', $_GET))
          $dt_end_param_name = 'dt_end';
      $dt_before = $this->convertTS($dt_end_param_name, $dt_tz);

      if (is_null($dt_after) && is_null($dt_before)) {
        $this->processDateTimePeriod($dt_tz, $dt_after, $dt_before);
      }

      // handle 'file' parameter
      $download_file_name = null;
      $file_param = array_key_exists('file', $_GET) ? $_GET['file'] : null;
      if (!is_null($file_param)) {
        if (preg_match('/^[-_a-zA-Z0-9]+$/', $file_param))
          $download_file_name = $file_param;
        else
          $this->renderErrorAndHelp(L("invalid file name: ") . $file_param . L(" (only alphbet, number, dash and underscore are valid)"));
      }

      $true_text = null;
      $false_text = null;
      $null_text = null;
      if (isset($_GET['true_text']))
        $true_text = $_GET['true_text'];
      if (isset($_GET['false_text']))
        $false_text = $_GET['false_text'];
      if (isset($_GET['null_text']))
        $null_text = $_GET['null_text'];
      
      $format = $_GET['format'];
      $formatter = build_formatter($format);
      if (is_null($formatter))
        $this->renderErrorAndHelp(L("invalid format: ") . $format);
      else
      {
        if (isset($_GET['save_to_local'])) {
          $file_name = $_GET['save_to_local'];
          if (preg_match('/^[-_a-zA-Z0-9]+$/', $file_name))  {
            $formatter->saveToLocal($file_name);
            if (isset($_GET['zip_threshold'])) {
              $zip_threshold = floatval($_GET['zip_threshold']);
              $formatter->setZipThreshold($zip_threshold);  
            }
          }
          else
            $this->renderErrorAndHelp(L("invalid file name: ") . $file_name . L(" (only alphbet, number, dash and underscore are valid)"));
        }
      }
    
      if (empty($table))
        $this->renderErrorAndHelp(L("'table' is invalid"));

      if (empty($columns))
        $columns = '*';

      //NOTE: convert dt in sqlite to get better performance
      $orig_columns = $columns;
      $tzOffset = $this->tzOffset($dt_tz);
      $convertDTExpr = "datetime(dt, '{$tzOffset} hours') as _dt";
      if ($columns == '*')
      {
        $columns = "*,{$convertDTExpr}";
      } else {
        $pieces = explode(",", $columns);
        $selectors = array();
        $foundDt = false;
        foreach($pieces as $piece) {
          $piece = trim($piece);
          if (strcmp($piece, 'dt') == 0)
            $foundDt = true;
          $selectors[] = "\"$piece\"";
        }
        if ($foundDt)
          $selectors[] = $convertDTExpr;
        $columns = implode(",", $selectors);
      }

      //build and do sql query
      $db = DBConn::instance()->history_db();
      $conditions = array();
      // if (isset($_GET['conditions']))
      //   $conditions[] = '(' . $_GET['conditions'] . ')';

      // $conditions[] = "dt >= :dt_after";
      // $conditions[] = "dt < :dt_before";
      if (!is_null($dt_after))
        $conditions[] = "dt >= '" . $dt_after->format('Y-m-d H:i:s') . "'";
      if (!is_null($dt_before))
        $conditions[] = "dt < '" . $dt_before->format('Y-m-d H:i:s') . "'";

      $query = "SELECT {$columns} FROM {$table}"
      foreach($_GET as $k=>$v) {
        if (in_array($k, $this->special_keys))
          continue;
        $conditions[] = "\"$k\"=:$k";
      }
      if ($conditions)
        $query = $query . ' WHERE ' . implode(' AND ', $conditions);
      
      if (isset($_GET['last_n_row']))
        $lastNRow = $_GET['last_n_row'];
      if (isset($lastNRow) && is_numeric($lastNRow)) {
        $lastNRow = min(10000, intval($lastNRow));
        $query = $query . ' ORDER BY dt DESC LIMIT ' . $lastNRow;
        $query = 'SELECT * FROM ( ' . $query . ') ORDER BY dt ASC;'
      } else {
        $query = $query . ' ORDER BY dt LIMIT 10000 ;';
      }
      $stmt = $db->prepare($query);

      if (!isset($stmt) || !$stmt) {
        $db_error = $db->errorInfo();
        if (count($db_error) != 3)
          $this->renderErrorAndHelp(L('unknown error'));
        else
          $this->renderErrorAndHelp(L('error: ') . $db_error[2]);
      }

      foreach($_GET as $k=>$v) {
        if (in_array($k, $this->special_keys))
          continue;
        $stmt->bindValue(":$k", $v);
      }

      // $stmt->bindValue(':dt_after', '"' . $dt_after->format('Y-m-d H:i:s') . '"');
      // $stmt->bindValue(':dt_before', '"' . $dt_before->format('Y-m-d H:i:s'). '"');

      // $etime = new ElapsedTime();

      $stmt->execute();
      // error_log("### elapsed after query: " . $etime->elapsed());
      
      $bool_text_mappings = array('0' => $false_text, '1' => $true_text, '2' => $null_text);
      output_history_data($stmt, $formatter, $download_file_name, $orig_columns, $bool_text_mappings, $db, $table);
    }

    protected function renderErrorAndHelp($error) {
        $style = <<<STYLEDOC
<style>
.error_text { color: red; }
table, th, td { border: 1px solid grey; }
th,td { padding:15px; }
</style>

STYLEDOC;

        $help_desc = <<<HELPDOC
<div>
    <p>data_exporter script will allow you to export data from database, the URL syntax looks like this: <b>http://DEVICE_IP/sdcard/cpt/app/data_exporter.php?PARAM=VALUEhttp://DEVICE_IP/sdcard/cpt/app/data_exporter.php?PARAM=VALUE&PARAM2=VALUE2...</b></p>amp;PARAM2=VALUE2...http://DEVICE_IP/sdcard/cpt/app/data_exporter.php?PARAM=VALUE&PARAM2=VALUE2...</b></p>lt;/bhttp://DEVICE_IP/sdcard/cpt/app/data_exporter.php?PARAM=VALUE&PARAM2=VALUE2...</b></p>gt;http://DEVICE_IP/sdcard/cpt/app/data_exporter.php?PARAM=VALUE&PARAM2=VALUE2...</b></p>lt;/phttp://DEVICE_IP/sdcard/cpt/app/data_exporter.php?PARAM=VALUE&PARAM2=VALUE2...</b></p>gt;

    <div>Parameter details:
    <table style="border:1px solid grey;">
        <thead>
            <tr>
                <td>Name</td>
                <td>Value</td>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>table</td>
                <td>name of the database table, required.</td>
            </tr>
            <tr>
                <td>columns</td>
                <td>column name list, seperated by ','. if missing will return all columns' data</td>
            </tr>
            <tr>
                <td>format</td>
                <td>returned data format, valid format value: csv, html, json. if missing, the default is html.</td>
            </tr>
            <tr>
                <td>file</td>
                <td>with this parameter in url value, the exported data will trigger a download file process in browser and the file's name will the content of this parameter; otherwise data will be rendered on browser page directly. for example, if this parameter is 'last_week_data' and 'format' parameter is 'csv', then the download file's default name will be 'last_week_data.csv'. if missing, the default is rendering on browser page.</td>
            </tr>
            <tr>
                <td>dt_period</td>
                <td>only export data within this period. valid period value: today, yesterday, last_24hr, week_to_date, last_week, month_to_date, last_month, year_to_date, last_year. if missing, will export all.</td>
            </tr>
            <tr>
                <td>dt_start</td>
                <td>export data after(inclusive) this timestamp. for example 20130515011025 will export data after  01:10:25 of May 15, 2013. if missing, will export data starting from the oldest record.</td>
            </tr>
            <tr>
                <td>dt_after</td>
                <td>the same as 'dt_start'.</td>
            </tr>
            <tr>
                <td>dt_before</td>
                <td>export data before(exclusive) this timestamp. the format is the same as 'dt_after'. if missing, will export data till now.</td>
            </tr>
            <tr>
                <td>dt_end</td>
                <td>the same as 'dt_before'.</td>
            </tr>
            <tr>
                <td>dt_tz</td>
                <td>the timezone of 'dt_start'/'dt_end' or 'dt_after'/'dt_before' parameters. NOTE: the '/' character in the timezone's name must be encoded as '%2F', for example, 'Asia/Shanghai' should be 'Asia%2FShanghai'. </td>
            </tr>
            <tr>
                <td>dt_week_start_on_sunday</td>
                <td>if this param passed in with '1' or 'true' as its value, week will start from Sunday, otherwise, week starts from Monday</td>
            </tr>
            <tr>
                <td>true_text</td>
                <td>replace any boolean column in TRUE value with given text</td>
            </tr>
            <tr>
                <td>false_text</td>
                <td>replace any boolean column in FALSE value with given text</td>
            </tr>
            <tr>
                <td>null_text</td>
                <td>replace any boolean column in NULL value with given text</td>
            </tr>
            <tr>
                <td>last_n_row</td>
                <td>only return the last N rows in the query, this parameter must be a valid integer</td>
            </tr>
            <tr>
                <td>save_to_local</td>
                <td>save exported data on controller, you can specify file's name here(for example: "last_month_temperature"). the response will be data file's full path on controller. </td>
            </tr>
            <tr>
                <td>auth_key</td>
                <td>ust specify auth_key to export data, ask it from admin of CPT Graphics. </td>
            </tr>
        </tbody>
    </table>
    </div>
</div>
HELPDOC;
        $error_msg = $style . "<div class='error_text'>ERROR: " . $error . "</div>" . $help_desc;
        $this->renderErrorResponse($error_msg);
    }
  };

  $controller = new DataExportController();
  $controller->run();

?>