/*
 * This file is part of Prism, licensed under the MIT License (MIT).
 *
 * Copyright (c) 2015 Helion3 http://helion3.com/
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.helion3.prism.storage.mysql;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;

import com.helion3.prism.Prism;
import com.helion3.prism.api.storage.StorageAdapter;
import com.helion3.prism.api.storage.StorageAdapterRecords;
import com.helion3.prism.api.storage.StorageAdapterSettings;
import com.helion3.prism.util.DataQueries;
import com.helion3.prism.util.DateUtil;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.spongepowered.api.scheduler.Task;

public class MySQLStorageAdapter implements StorageAdapter {

    private final String expiration = Prism.getInstance().getConfig().getStorageCategory().getExpireRecords();
    private final String tablePrefix = Prism.getInstance().getConfig().getStorageCategory().getTablePrefix();
    private final int purgeBatchLimit = Prism.getInstance().getConfig().getStorageCategory().getPurgeBatchLimit();
    private final StorageAdapterRecords records;
    private static HikariDataSource db;
    private final String dns;

    /**
     * Create a new instance of the H2 storage adapter.
     */
    public MySQLStorageAdapter() {
        records = new MySQLRecords();

        dns = String.format("jdbc:mysql://%s/%s",
                Prism.getInstance().getConfig().getStorageCategory().getAddress(),
                Prism.getInstance().getConfig().getStorageCategory().getDatabase()
        );
    }

    /**
     * Get the connection.
     *
     * @return Connection
     * @throws SQLException
     */
    protected static Connection getConnection() throws SQLException {
        return db.getConnection();
    }

    @Override
    public boolean connect() throws Exception {
        try {
            // Get data source
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl(dns);
            String mysqlDriver = Prism.getInstance().getConfig().getStorageCategory().getMysqlDriver();
            if (mysqlDriver.equalsIgnoreCase("MySQL")) {
                config.setDriverClassName("com.mysql.cj.jdbc.Driver");
            } else if (mysqlDriver.equalsIgnoreCase("MariaDB")) {
                config.setDriverClassName("org.mariadb.jdbc.Driver");
            } else {
                Prism.getInstance().getLogger().error("Invalid input for MySQL Driver configuration: " + mysqlDriver);
            }
            config.setUsername(Prism.getInstance().getConfig().getStorageCategory().getUsername());
            config.setPassword(Prism.getInstance().getConfig().getStorageCategory().getPassword());
            config.setMaximumPoolSize(Prism.getInstance().getConfig().getStorageCategory().getMaximumPoolSize());
            config.setMinimumIdle(Prism.getInstance().getConfig().getStorageCategory().getMinimumIdle());

            db = new HikariDataSource(config);

            // Create table if needed
            createTables();

            // Purge async
            if (Prism.getInstance().getConfig().getStorageCategory().isShouldExpire()) {
                Task.builder()
                    .async()
                    .name("PrismMySQLPurge")
                    .execute(this::purge)
                    .submit(Prism.getInstance().getPluginContainer());
            }

            return true;
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Create table structure if none present.
     *
     * @throws SQLException
     */
    protected void createTables() throws SQLException {
        try (Connection conn = getConnection()) {
            String records = "CREATE TABLE IF NOT EXISTS "
                    + tablePrefix + "records ("
                    + "id int(10) unsigned NOT NULL AUTO_INCREMENT, "
                    + DataQueries.Created + " int(10) unsigned NOT NULL, "
                    + DataQueries.EventName + " varchar(16) NOT NULL, "
                    + DataQueries.WorldUuid + " binary(16) NOT NULL, "
                    + DataQueries.X + " int(10) NOT NULL, "
                    + DataQueries.Y + " smallint(5) NOT NULL, "
                    + DataQueries.Z + " int(10) NOT NULL, "
                    + DataQueries.Target + " varchar(255), "
                    + DataQueries.Player + " binary(16), "
                    + DataQueries.Cause + " varchar(55), "
                    + "PRIMARY KEY (`id`), "
                    + "KEY  `location` (`"+ DataQueries.WorldUuid
                        + "`, `" + DataQueries.X
                        + "`, `" + DataQueries.Z
                        + "`, `" + DataQueries.Y
                    + "`), "
                    + "KEY `created` (`created`)"
                    + ") ENGINE=InnoDB DEFAULT CHARACTER SET utf8 " +
                    "  DEFAULT COLLATE utf8_general_ci;";
            conn.prepareStatement(records).execute();

            String extra = "CREATE TABLE IF NOT EXISTS "
                    + tablePrefix + "extra ("
                    + "id int(10) unsigned NOT NULL AUTO_INCREMENT, "
                    + "record_id int(10) unsigned NOT NULL, "
                    + "json TEXT, "
                    + "PRIMARY KEY (`id`), "
                    + "KEY `record_id` (`record_id`), "
                    + "CONSTRAINT " + tablePrefix + "extra_ibfk_1 "
                    + "FOREIGN KEY (record_id) "
                    + "REFERENCES " + tablePrefix + "records (id) "
                    + "ON DELETE CASCADE"
                    + ") ENGINE=InnoDB DEFAULT CHARACTER SET utf8 "
                    + "DEFAULT COLLATE utf8_general_ci;";
            conn.prepareStatement(extra).execute();

            if (Prism.getInstance().getConfig().getGeneralCategory().getSchemaVersion() == 1) {
                // Expand target: 55 -> 255
                conn.prepareStatement(String.format("ALTER TABLE %srecords MODIFY %s varchar(255);",
                        tablePrefix,
                        DataQueries.Target
                )).execute();

                Prism.getInstance().getConfig().getGeneralCategory().setSchemaVersion(2);
                Prism.getInstance().getConfiguration().saveConfiguration();
            }
        }
    }

    /**
     * Removes expires records and extra information from the database.
     */
    protected void purge() {
        try {
            Prism.getInstance().getLogger().info("Purging MySQL database...");
            long purged = 0;
            while (true) {
                int count = purgeRecords();
                if (count == 0) {
                    break;
                }

                purged += count;
                Prism.getInstance().getLogger().info("Deleted {} records", purged);
            }

            Prism.getInstance().getLogger().info("Finished purging MySQL database");
        } catch (Exception ex) {
            Prism.getInstance().getLogger().error("Encountered an error while purging MySQL database", ex);
        }
    }

    /**
     * Removes expires records from the database.
     *
     * @return The amount of rows removed.
     * @throws Exception
     */
    protected int purgeRecords() throws Exception {
        Date date = DateUtil.parseTimeStringToDate(expiration, false);
        if (date == null) {
            throw new IllegalArgumentException("Failed to parse expiration");
        }

        if (purgeBatchLimit <= 0) {
            throw new IllegalArgumentException("PurgeBatchLimit cannot be equal to or lower than 0");
        }

        String sql = "DELETE FROM " + tablePrefix + "records "
                + "WHERE " + tablePrefix + "records.created <= ? "
                + "LIMIT ?;";
        try (Connection conn = getConnection(); PreparedStatement statement = conn.prepareStatement(sql)) {
            statement.setLong(1, date.getTime() / 1000);
            statement.setInt(2, purgeBatchLimit);
            return statement.executeUpdate();
        }
    }

    @Override
    public StorageAdapterRecords records() {
        return records;
    }

    @Override
    public StorageAdapterSettings settings() {
        // @todo implement
        return null;
    }

    @Override
    public void close() {
        db.close();
    }

    @Override
    public boolean testConnection() throws Exception {
        // @todo implement
        return true;
    }
}