/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.druid.mock;

import com.alibaba.druid.mock.handler.MockExecuteHandler;
import com.alibaba.druid.mock.handler.MySqlMockExecuteHandlerImpl;
import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;

import javax.management.MBeanServer;
import javax.management.ObjectName;

import java.lang.management.ManagementFactory;
import java.sql.*;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;

public class MockDriver implements Driver, MockDriverMBean {
    private static Log LOG;

    public static final MockExecuteHandler DEFAULT_HANDLER = new MySqlMockExecuteHandlerImpl();

    private String prefix = "jdbc:fake:";
    private String mockPrefix = "jdbc:mock:";

    private MockExecuteHandler executeHandler = DEFAULT_HANDLER;

    public static final MockDriver instance = new MockDriver();

    private final AtomicLong connectCount = new AtomicLong();
    private final AtomicLong connectionCloseCount = new AtomicLong();

    private final AtomicLong connectionIdSeed = new AtomicLong(1000L);

    private final List<MockConnection> connections = new CopyOnWriteArrayList<MockConnection>();

    private long idleTimeCount = 1000 * 60 * 3;

    private boolean logExecuteQueryEnable = true;

    private static final String MBEAN_NAME = "com.alibaba.druid:type=MockDriver";

    static {
        registerDriver(instance);
    }

    public boolean isLogExecuteQueryEnable() {
        return logExecuteQueryEnable;
    }

    private static Log getLog() {
        if (LOG == null) {
            LOG = LogFactory.getLog(MockDriver.class);
        }

        return LOG;
    }

    public void setLogExecuteQueryEnable(boolean logExecuteQueryEnable) {
        this.logExecuteQueryEnable = logExecuteQueryEnable;
    }

    public long getIdleTimeCount() {
        return idleTimeCount;
    }

    public void setIdleTimeCount(long idleTimeCount) {
        this.idleTimeCount = idleTimeCount;
    }

    public long generateConnectionId() {
        return connectionIdSeed.incrementAndGet();
    }

    public void closeAllConnections() throws SQLException {
        for (int i = 0, size = this.connections.size(); i < size; ++i) {
            Connection conn = this.connections.get(size - i - 1);
            conn.close();
        }
    }

    public int getConnectionsSize() {
        return this.connections.size();
    }

    public List<MockConnection> getConnections() {
        return connections;
    }

    protected void incrementConnectionCloseCount() {
        connectionCloseCount.incrementAndGet();
    }

    public long getConnectionCloseCount() {
        return connectionCloseCount.get();
    }

    protected void afterConnectionClose(MockConnection conn) {
        connectionCloseCount.incrementAndGet();

        connections.remove(conn);

        if (getLog().isDebugEnabled()) {
            getLog().debug("conn-" + conn.getId() + " close");
        }
    }

    public static boolean registerDriver(Driver driver) {
        try {
            DriverManager.registerDriver(driver);

            try {
                MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();

                ObjectName objectName = new ObjectName(MBEAN_NAME);
                if (!mbeanServer.isRegistered(objectName)) {
                    mbeanServer.registerMBean(instance, objectName);
                }
            } catch (Exception ex) {
                getLog().warn("register druid-driver mbean error", ex);
            }

            return true;
        } catch (Exception e) {
            getLog().error("registerDriver error", e);
        }

        return false;
    }

    public MockExecuteHandler getExecuteHandler() {
        return executeHandler;
    }

    public void setExecuteHandler(MockExecuteHandler executeHandler) {
        this.executeHandler = executeHandler;
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        if (!acceptsURL(url)) {
            return null;
        }

        if (info != null) {
            Object val = info.get("connectSleep");
            if (val != null) {
                long millis = Long.parseLong(val.toString());
                try {
                    Thread.sleep(millis);
                } catch (InterruptedException e) {
                    // skip
                }
            }
        }

        MockConnection conn = createMockConnection(this, url, info);

        if (getLog().isDebugEnabled()) {
            getLog().debug("connect, url " + url + ", id " + conn.getId());
        }

        if (url == null) {
            connectCount.incrementAndGet();
            connections.add(conn);
            return conn;
        }

        if (url.startsWith(prefix)) {
            String catalog = url.substring(prefix.length());
            conn.setCatalog(catalog);

            connectCount.incrementAndGet();
            connections.add(conn);
            return conn;
        }

        if (url.startsWith(mockPrefix)) {
            String catalog = url.substring(mockPrefix.length());
            conn.setCatalog(catalog);

            connectCount.incrementAndGet();
            connections.add(conn);
            return conn;
        }

        return null;
    }

    @Override
    public boolean acceptsURL(String url) throws SQLException {
        if (url == null) {
            return false;
        }

        return url.startsWith(prefix) || url.startsWith(mockPrefix);
    }

    @Override
    public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
        return null;
    }

    @Override
    public int getMajorVersion() {
        return 0;
    }

    @Override
    public int getMinorVersion() {
        return 0;
    }

    @Override
    public boolean jdbcCompliant() {
        return true;
    }

    public MockResultSet createMockResultSet(MockStatementBase stmt) {
        return new MockResultSet(stmt);
    }

    public ResultSet executeQuery(MockStatementBase stmt, String sql) throws SQLException {
        if (logExecuteQueryEnable && getLog().isDebugEnabled()) {
            getLog().debug("executeQuery " + sql);
        }

        MockConnection conn = stmt.getConnection();

        long idleTimeMillis = System.currentTimeMillis() - conn.getLastActiveTimeMillis();
        if (idleTimeMillis >= this.idleTimeCount) {
            throw new SQLException("connection is idle time count");
        }

        conn.setLastActiveTimeMillis(System.currentTimeMillis());

        handleSleep(conn);

        if ("SELECT value FROM _int_1000_".equalsIgnoreCase(sql)) {
            MockResultSet rs = createMockResultSet(stmt);

            for (int i = 0; i < 1000; ++i) {
                rs.getRows().add(new Object[]{i});
            }

            return rs;
        }

        return this.executeHandler.executeQuery(stmt, sql);
    }

    public void handleSleep(MockConnection conn) {
        if (conn != null) {
            conn.handleSleep();
        }
    }

    public ResultSet createResultSet(MockPreparedStatement stmt) {
        MockResultSet rs = new MockResultSet(stmt);

        String sql = stmt.getSql();

        if ("SELECT 1".equalsIgnoreCase(sql)) {
            rs.getRows().add(new Object[]{1});
        } else if ("SELECT NOW()".equalsIgnoreCase(sql)) {
            rs.getRows().add(new Object[]{new java.sql.Timestamp(System.currentTimeMillis())});
        } else if ("SELECT ?".equalsIgnoreCase(sql)) {
            rs.getRows().add(new Object[]{stmt.getParameters().get(0)});
        }

        return rs;
    }

    protected Clob createClob(MockConnection conn) throws SQLException {
        return new MockClob();
    }

    protected Blob createBlob(MockConnection conn) throws SQLException {
        return new MockBlob();
    }

    protected NClob createNClob(MockConnection conn) throws SQLException {
        return new MockNClob();
    }

    protected SQLXML createSQLXML(MockConnection conn) throws SQLException {
        return new MockSQLXML();
    }

    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        throw new SQLFeatureNotSupportedException();
    }

    public MockConnection createMockConnection(MockDriver driver, String url, Properties connectProperties) {
        return new MockConnection(this, url, connectProperties);
    }

    public MockPreparedStatement createMockPreparedStatement(MockConnection conn, String sql) {
        return new MockPreparedStatement(conn, sql);
    }

    public MockStatement createMockStatement(MockConnection conn) {
        return new MockStatement(conn);
    }

    public MockCallableStatement createMockCallableStatement(MockConnection conn, String sql) {
        return new MockCallableStatement(conn, sql);
    }
}
