<?php

abstract class datatable
{
    private static $table = array(
        "name" => null,
        "primary" => null,
        "creators" => [],
        "editors" => []
    );

    private static $data = array();

    public static function data()
    {
        return static::$data;
    }

    // default values for each data field
    public static function defaults($override = null)
    {
        $defaults = [];

        foreach (static::data() as $key => $value) {

            if (isset($override[$key])) {
                $defaults[$key] = $override[$key];
            } else if (isset($value["default"])) {

                switch ($value["default"]) {
                    case '#timestamp':
                        $defaults[$key] = timestamp();
                        break;
                    case '#user':
                        $defaults[$key] = auth::user() ? auth::user_id() : 1;
                        break;
                    case '#userinstitution':
                        $defaults[$key] = auth::user() ? auth::user_institution() : 1;
                        break;
                    case '#date':
                        $defaults[$key] = date('Y-m-d');
                        break;
                    case '#time':
                        $defaults[$key] = date('H:i:s');
                        break;
                    default:
                        $defaults[$key] = $value["default"];
                        break;
                }
            } else {
                $defaults[$key] = null;
            }
        }
        return $defaults;
    }

    public static function create($inputs)
    {
        // TODO - Check permission to create

        $response["action"] = "create";
        $errors = [];

        $inputs = static::filter_inputs($inputs);
        $inputs = static::modify_inputs($inputs);

        // specific to create action
        $inputs = array_replace(static::defaults(), $inputs);

        $data = static::validate_inputs($inputs);
        $response["data"] = $data["data"];

        if ($data["allvalid"]) {

            $keys = implode(", ", array_keys($inputs));
            $values  = ":" . implode(", :", array_keys($inputs));
            $sql = "INSERT INTO " . static::$table['name'] . " (" . $keys . ") VALUES (" . $values . ")";

            try {
                $insert = dbconnection::get()->prepare($sql)->execute($inputs);
                if ($insert) {
                    $response['success'] = true;
                    $response['id'] = dbconnection::get()->lastInsertId();
                } else {
                    $response["success"] = false;
                    $errors[] = "A: " . $insert->errorInfo()[2];
                }
            } catch (PDOException $e) {
                $response["success"] = false;
                $errors[] = "catch: " . $e->getMessage();
            }
        } else {
            $errors[] = "Invalid Input found, New Record Creation Failed";
        }

        if (!empty($errors)) {
            $response["errors"] = $errors;
            $response["success"] = false;
        } else {
            $response["success"] = true;
        }

        return $response;
    }


    public static function update($id, $inputs)
    {
        $response["action"] = "update";
        $response["id"] = $id;
        //$response["inputs"]["raw"] = $inputs;

        $errors = [];
        $warnings = [];

        $inputs = static::filter_inputs($inputs);
        //$response["inputs"]["filtered"] = $inputs;

        $inputs = static::modify_inputs($inputs);
        //$response["inputs"]["modified"] = $inputs;



        $data_existing = static::get($id);

        if ($data_existing) {

            // Filter changed data fields
            $data = [];
            $inputs_changed = [];
            foreach ($inputs as $field => $value) {
                if ($data_existing[$field] != $value) {
                    $inputs_changed[$field] = $inputs[$field];
                }
            }

            $response["inputs"]["changed"] =  $inputs_changed;

            if (empty($inputs_changed)) {
                $warnings[] = "Nothing to update";
            } else {

                $validation = static::validate_inputs($inputs_changed);

                $data = $validation["data"];
                foreach ($data as $field => $value) {
                    if ($value["valid"]) {
                        try {
                            $update = dbconnection::get()->prepare("update " . static::$table['name'] . " set " . $field . " = :input where " . static::$table['primary'] . " = :primary_key");
                            if ($update->execute([
                                "primary_key" => $id,
                                "input" => $value["input"]
                            ])) {
                                $data[$field]["updated"] = true;
                                //TODO: log the update
                            } else {
                                $data[$field]['error'] = $update->errorInfo()[2];
                            }
                        } catch (PDOException $e) {
                            $data[$field]['error'] = $e->getMessage();
                        }
                    }
                }

                $response["success"] = true;
            }
        } else {
            $errors[] = "Requested data does not exist";
        }

        $response["data"] = $data;

        if (!empty($errors)) {
            $response["errors"] = $errors;
            $response["success"] = false;
        } else {
            $response["success"] = true;
        }

        if (!empty($warnings)) {
            $response["warnings"] = $warnings;
        }

        return $response;
    }

    // Filter out data fields which are not supposed to be user inputs for create/update
    public static function filter_inputs($inputs)
    {
        foreach ($inputs as $field => $value) {
            // Remove none defined data fields
            if (!isset(static::data()[$field])) {
                unset($inputs[$field]);
            } // Remove locked data fields
            else if (isset(static::data()[$field]["locked"])) {
                unset($inputs[$field]);
            }
        }

        return $inputs;
    }

    // Modify raw inputs to desired values
    public static function modify_inputs($inputs)
    {

        // replace empty values and spaces with null
        foreach ($inputs as $key => $value) {
            if (is_int($value)) {
                continue;
            }

            if ($value == "" || ctype_space($value) || empty($value)) {
                $inputs[$key] = null;
            }
        }

        if (isset(static::data()["updated_at"])) {
            $inputs["updated_at"] = timestamp();
        }

        if (isset(static::data()["updated_by"])) {
            $inputs["updated_by"] = auth::user_id();
        }

        return $inputs;
    }

    // validate inputs
    public static function validate_inputs($inputs)
    {
        $data = [];
        $response["allvalid"] = true;

        foreach ($inputs as $field => $input) {

            $data[$field]["input"] = $input;
            $warnings = [];

            // What if input can be null, but has validators
            if (empty($input) && !isset(static::data()[$field]["required"])) {
                //continue;
            } else if (isset(static::data()[$field]["validators"])) {
                foreach (static::data()[$field]["validators"] as $v) {
                    if (method_exists("validator", $v)) {
                        $validation = validator::$v($input);
                        if (!$validation["success"]) {
                            $warnings = array_merge($warnings, $validation["warnings"]); // Merging not needed if validation stops at the first unsuccessful validator
                            //break; // stop further valiidation checks
                        }
                    }
                }
            }

            if (empty($warnings)) {
                $data[$field]["valid"] = 1;
            } else {
                $data[$field]["valid"] = 0;
                $data[$field]["warnings"] = $warnings;
                $response["allvalid"] = false;
            }
        }

        $response["data"] = $data;
        return $response;
    }

    // TODO - Move this method to database class
    public static function query($sql, $data = false)
    {
        $return['input'] = $data;

        //dbconnection::get()->query("SET SESSION SQL_BIG_SELECTS=1");

        $execute = dbconnection::get()->prepare($sql);

        if ($data) {
            $e = $execute->execute($data);
        } else {
            $e = $execute->execute();
        }

        if ($e) {
            $return['success'] = true;
            $return['data'] = $execute->fetchall(PDO::FETCH_ASSOC);
            $return['count'] = $execute->rowCount();
            if ($return['count'] == 0) {
                $return['empty'] = true;
            }
        } else {
            $return['success'] = false;
            $return['error'] = $execute->errorInfo()[2];
        }

        return $return;
    }

    public static function get($id)
    {
        $data =  self::query("select * from " . static::$table['name'] . " where " . static::$table['primary'] . " = :" . static::$table['primary'], [static::$table['primary'] => $id]);

        if ($data['success'] && $data['count'] == 1) {
            return $data['data'][0];
        } else {
            return false;
        }
    }

    public static function getall()
    {
        $data =  self::query("select * from " . static::$table['name'] . " order by " . static::$table['primary']);

        if ($data['success']) {
            return $data['data'];
        } else {
            return false;
        }
    }

    public static function exists($id)
    {
        $response =  self::query("select * from " . static::$table['name'] . " where " . static::$table['primary'] . " = :id", ["id" => $id]);

        if ($response['success'] && $response['count'] == 1) {
            return true;
        } else {
            return false;
        }
    }
}
