<?php
/**
 * OracleSQLTranslator.php
 *
 * A translator from MySQL dialect into Oracle dialect for Limesurvey
 * (http://www.limesurvey.org/)
 *
 * Copyright (c) 2012, André Rothe <arothe@phosco.info, phosco@gmx.de>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */

require_once dirname(__FILE__) . '/../src/PHPSQLParser.php';
require_once dirname(__FILE__) . '/../src/PHPSQLCreator.php';
include_once $rootdir . '/classes/adodb/adodb.inc.php';

$_ENV['DEBUG'] = 1;

/**
 * This class enhances the PHPSQLCreator to translate incoming
 * parser output into another SQL dialect (here: Oracle SQL).
 * 
 * @author arothe
 *
 */
class OracleSQLTranslator extends PHPSQLCreator {

    private $con; # this is the database connection from LimeSurvey
    private $preventColumnRefs = array();
    private $allTables = array();
    const ASTERISK_ALIAS = "[#RePl#]";

    public function __construct($con) {
        parent::__construct();
        $this->con = $con;
        $this->initGlobalVariables();
    }

    private function initGlobalVariables() {
        $this->preventColumnRefs = false;
        $this->allTables = array();
    }

    public static function dbgprint($txt) {
        if (isset($_ENV['DEBUG'])) {
            print $txt;
        }
    }

    public static function preprint($s, $return = false) {
        $x = "<pre>";
        $x .= print_r($s, 1);
        $x .= "</pre>";
        if ($return) {
            return $x;
        }
        self::dbgprint($x . "<br/>\n");
    }

    protected function processAlias($parsed) {
        if ($parsed === false) {
            return "";
        }
        # we don't need an AS between expression and alias
        $sql = " " . $parsed['name'];
        return $sql;
    }

    protected function processDELETE($parsed) {
        if (count($parsed['TABLES']) > 1) {
            die("cannot translate delete statement into Oracle dialect, multiple tables are not allowed.");
        }
        return "DELETE";
    }

    public static function getColumnNameFor($column) {
        if (strtolower($column) === 'uid') {
            $column = "uid_";
        }
        // TODO: add more here, if necessary
        return $column;
    }

    public static function getShortTableNameFor($table) {
        if (strtolower($table) === 'surveys_languagesettings') {
            $table = 'surveys_lngsettings';
        }
        // TODO: add more here, if necessary     
        return $table;
    }

    protected function processTable($parsed, $index) {
        if ($parsed['expr_type'] !== 'table') {
            return "";
        }

        $sql = $this->getShortTableNameFor($parsed['table']);
        $alias = $this->processAlias($parsed['alias']);
        $sql .= $alias;

        if ($index !== 0) {
            $sql = $this->processJoin($parsed['join_type']) . " " . $sql;
            $sql .= $this->processRefType($parsed['ref_type']);
            $sql .= $this->processRefClause($parsed['ref_clause']);
        }

        # store the table and its alias for later use
        $last = array_pop($this->allTables);
        $last['tables'][] = array('table' => $this->getShortTableNameFor($parsed['table']), 'alias' => trim($alias));
        $this->allTables[] = $last;

        return $sql;
    }

    protected function processFROM($parsed) {
        $this->allTables[] = array('tables' => array(), 'alias' => '');
        return parent::processFROM($parsed);
    }

    protected function processTableExpression($parsed, $index) {
        if ($parsed['expr_type'] !== 'table_expression') {
            return "";
        }
        $sql = substr($this->processFROM($parsed['sub_tree']), 5); // remove FROM keyword
        $sql = "(" . $sql . ")";

        $alias .= $this->processAlias($parsed['alias']);
        $sql .= $alias;

        # store the tables-expression-alias for later use
        $last = array_pop($this->allTables);
        $last['alias'] = trim($alias);
        $this->allTables[] = $last;

        if ($index !== 0) {
            $sql = $this->processJoin($parsed['join_type']) . " " . $sql;
            $sql .= $this->processRefType($parsed['ref_type']);
            $sql .= $this->processRefClause($parsed['ref_clause']);
        }
        return $sql;
    }

    private function getTableNameFromExpression($expr) {
        $pos = strpos($expr, ".");
        if ($pos === false) {
            $pos = -1;
        }
        return trim(substr($expr, 0, $pos + 1), ".");
    }

    private function getColumnNameFromExpression($expr) {
        $pos = strpos($expr, ".");
        if ($pos === false) {
            $pos = -1;
        }
        return substr($expr, $pos + 1);
    }

    private function isCLOBColumnInDB($table, $column) {
        $res = $this->con->GetOne(
                "SELECT count(*) FROM user_lobs WHERE table_name='" . strtoupper($table) . "' AND column_name='"
                        . strtoupper($column) . "'");
        return ($res >= 1);
    }

    protected function isCLOBColumn($table, $column) {
        $tables = end($this->allTables);

        if ($table === "") {
            foreach ($tables['tables'] as $k => $v) {
                if ($this->isCLOBColumn($v['table'], $column)) {
                    return true;
                }
            }
            return false;
        }

        # check the aliases, $table cannot be empty
        foreach ($tables['tables'] as $k => $v) {
            if ((strtolower($v['alias']) === strtolower($table))
                    || (strtolower($tables['alias']) === strtolower($table))) {
                if ($this->isCLOBColumnInDB($v['table'], $column)) {
                    return true;
                }
            }
        }

        # it must be a valid table name
        return $this->isCLOBColumnInDB($table, $column);
    }

    protected function processOrderByExpression($parsed) {
        if ($parsed['expr_type'] !== 'expression') {
            return "";
        }

        $table = $this->getTableNameFromExpression($parsed['base_expr']);
        $col = $this->getColumnNameFromExpression($parsed['base_expr']);

        $sql = ($table !== "" ? $table . "." : "") . $col;

        # check, if the column is a CLOB
        if ($this->isCLOBColumn($table, $col)) {
            $sql = "cast(substr(" . $sql . ",1,200) as varchar2(200))";
        }

        return $sql . " " . $parsed['direction'];
    }

    protected function processColRef($parsed) {
        if ($parsed['expr_type'] !== 'colref') {
            return "";
        }

        $table = $this->getTableNameFromExpression($parsed['base_expr']);
        $col = $this->getColumnNameFromexpression($parsed['base_expr']);

        # we have to change the column name, if the column is uid
        # we have to change the tablereference, if the tablename is too long
        $col = $this->getColumnNameFor($col);
        $table = $this->getShortTableNameFor($table);

        # if we have * as colref, we cannot use other columns
        # we have to add alias.* if we know all table aliases
        if (($table === "") && ($col === "*")) {
            array_pop($this->preventColumnRefs);
            $this->preventColumnRefs[] = true;
            return ASTERISK_ALIAS; # this is the position, we have to replace later
        }

        $alias = "";
        if (isset($parsed['alias'])) {
            $alias = $this->processAlias($parsed['alias']);
        }

        return (($table !== "") ? ($table . "." . $col) : $col) . $alias;
    }

    protected function processFunctionOnSelect($parsed) {
        $old = end($this->preventColumnRefs);
        $sql = $this->processFunction($parsed);

        if ($old !== end($this->preventColumnRefs)) {
            # prevents wrong handling of count(*)
            array_pop($this->preventColumnRefs);
            $this->preventColumnRefs[] = $old;
            $sql = str_replace(ASTERISK_ALIAS, "*", $sql);
        }
        return $sql;
    }

    protected function processSELECT($parsed) {
        $this->preventColumnRefs[] = false;

        $sql = "";
        foreach ($parsed as $k => $v) {
            $len = strlen($sql);
            $sql .= $this->processColRef($v);
            $sql .= $this->processSelectExpression($v);
            $sql .= $this->processFunctionOnSelect($v);
            $sql .= $this->processConstant($v);

            if ($len == strlen($sql)) {
                $this->stop('SELECT', $k, $v, 'expr_type');
            }

            $sql .= ",";
        }
        $sql = substr($sql, 0, -1);
        return "SELECT " . $sql;
    }

    private function correctColRefStatement($sql) {
        $alias = "";
        $tables = end($this->allTables);

        # should we correct the selection list?
        if (array_pop($this->preventColumnRefs)) {

            # do we have a table-expression alias?
            if ($tables['alias'] !== "") {
                $alias = $tables['alias'] . ".*";
            } else {
                foreach ($tables['tables'] as $k => $v) {
                    $alias .= ($v['alias'] === "" ? $v['table'] : $v['alias']) . ".*,";
                }
                $alias = substr($alias, 0, -1);
            }
            $sql = str_replace(ASTERISK_ALIAS, $alias, $sql);
        }
        return $sql;
    }

    protected function processSelectStatement($parsed) {
        $sql = $this->processSELECT($parsed['SELECT']);
        $from = $this->processFROM($parsed['FROM']);

        # correct * references with tablealias.*
        # this must be called after processFROM(), because we need the table information
        $sql = $this->correctColRefStatement($sql) . " " . $from;

        if (isset($parsed['WHERE'])) {
            $sql .= " " . $this->processWHERE($parsed['WHERE']);
        }
        if (isset($parsed['GROUP'])) {
            $sql .= " " . $this->processGROUP($parsed['GROUP']);
        }
        if (isset($parsed['ORDER'])) {
            $sql .= " " . $this->processORDER($parsed['ORDER']);
        }

        # select finished, we remove its tables
        #  FIXME: we should add it to the previous tablelist with the
        #  global alias, if such one exists
        array_pop($this->allTables);

        return $sql;
    }

    public function create($parsed) {
        $k = key($parsed);
        switch ($k) {
        case "USE":
        # this statement is not an Oracle statement
            $this->created = "";
            break;

        default:
            $this->created = parent::create($parsed);
            break;
        }
        return $this->created;
    }

    public function process($sql) {
        self::dbgprint($sql . "<br/>");

        $this->initGlobalVariables();
        $parser = new PHPSQLParser($sql);
        self::preprint($parser->parsed);

        $sql = $this->create($parser->parsed);
        self::dbgprint($sql . "<br/>");

        return $sql;
    }
}

//$translator = new OracleSQLTranslator(false);
//$translator->process("SELECT q.qid, question, gid FROM questions as q WHERE (select count(*) from answers as a where a.qid=q.qid and scale_id=0)=0 and sid=11929 AND type IN ('F', 'H', 'W', 'Z', '1') and q.parent_qid=0");
//$translator->process("SELECT *, (SELECT a from xyz WHERE b>1) haha, (SELECT *, b from zks,abc WHERE d=1) hoho FROM blubb d, blibb c WHERE d.col = c.col");

?>