/*
 * Copyright 2015-2019 the original author or authors.
 *
 * 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 org.lastaflute.db.jta.romanticist;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.transaction.xa.Xid;

import org.dbflute.util.DfReflectionUtil;
import org.dbflute.util.Srl;
import org.lastaflute.db.jta.RomanticTransaction;
import org.lastaflute.jta.dbcp.ConnectionWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author jflute
 */
public class TransactionRomanticSnapshotBuilder {

    // ===================================================================================
    //                                                                          Definition
    //                                                                          ==========
    private static final Logger logger = LoggerFactory.getLogger(TransactionRomanticSnapshotBuilder.class);

    // ===================================================================================
    //                                                                            Romantic
    //                                                                            ========
    /**
     * @param tx The transaction it looks so romantic. (NotNull)
     * @param wrapper The wrapper of connection for the transaction to extract native process ID. (NotNull: but no check just in case)
     * @return The romantic expression for transaction snapshot. (NotNull)
     */
    public String buildRomanticSnapshot(RomanticTransaction tx, ConnectionWrapper wrapper) {
        final StringBuilder sb = new StringBuilder();
        sb.append("{").append(currentElapsedTimeExp(tx));
        setupXidExp(sb, tx, wrapper);
        setupEntryMethodExp(sb, tx);
        setupUserBeanExp(sb, tx);
        setupTableCommandExp(sb, tx);
        setupRequestPathExp(sb, tx); // might have long query string so last
        sb.append("}@").append(toHexHashExp(tx)).append("@").append(toHexHashExp(wrapper));
        // SQL display has lines so close here
        setupCurrentSqlExp(sb, tx);
        return sb.toString();
    }

    protected String toHexHashExp(Object obj) {
        return obj != null ? Integer.toHexString(obj.hashCode()) : "null";
    }

    // ===================================================================================
    //                                                                        Elapsed Time
    //                                                                        ============
    protected String currentElapsedTimeExp(RomanticTransaction tx) {
        return tx.currentElapsedTimeExp();
    }

    // ===================================================================================
    //                                                                                XID
    //                                                                               =====
    protected void setupXidExp(StringBuilder sb, RomanticTransaction tx, ConnectionWrapper wrapper) {
        sb.append(", ");
        final Xid xid = tx.getXid();
        final byte[] globalId = xid.getGlobalTransactionId();
        final byte[] branchId = xid.getBranchQualifier();
        if (globalId != null) {
            sb.append(new String(globalId).trim());
        } else { // no way, just in case
            sb.append("*no globalId");
        }
        if (branchId != null && branchId.length > 0) {
            final String branchStr = new String(branchId).trim();
            if (!branchStr.isEmpty()) { // might be only spaces
                sb.append("-" + branchStr);
            }
        }
        final Object processId = extractDbmsNativeProcessId(tx, wrapper);
        if (processId != null) {
            sb.append("(").append(processId).append(")");
        }
    }

    protected Object extractDbmsNativeProcessId(RomanticTransaction tx, ConnectionWrapper wrapper) {
        if (wrapper == null) { // just in case
            return null;
        }
        final Connection physicalConn = wrapper.getPhysicalConnection();
        if (physicalConn == null) { // just in case
            return null;
        }
        try {
            return digUpDbmsNativeProcessIdOf(physicalConn);
        } catch (RuntimeException continued) {
            logger.debug("Failed to get the DBMS native process ID: " + physicalConn, continued);
        }
        return null;
    }

    protected Object digUpDbmsNativeProcessIdOf(Connection physicalConn) {
        final Class<?> connType = physicalConn.getClass();
        if (getMySQLConnectionClassFQCN().equals(connType.getName())) {
            final Method method = DfReflectionUtil.getPublicMethod(connType, "getId", (Class<?>[]) null);
            return DfReflectionUtil.invoke(method, physicalConn, (Object[]) null);
        }
        return null;
    }

    protected String getMySQLConnectionClassFQCN() {
        return "com.mysql.jdbc.JDBC4Connection";
    }

    // ===================================================================================
    //                                                                        Entry Method
    //                                                                        ============
    protected void setupEntryMethodExp(StringBuilder sb, RomanticTransaction tx) {
        final Method entryMethod = tx.getEntryMethod();
        if (entryMethod != null) {
            final String appPkg = getApplicationPackageKeyword();
            final String classExp = Srl.substringFirstRear(entryMethod.getDeclaringClass().getName(), appPkg);
            sb.append(", ").append(classExp).append("@").append(entryMethod.getName()).append("()");
        }
    }

    protected String getApplicationPackageKeyword() {
        return ".app.";
    }

    // ===================================================================================
    //                                                                           User Bean
    //                                                                           =========
    protected void setupUserBeanExp(StringBuilder sb, RomanticTransaction tx) {
        final Object userBean = tx.getUserBean();
        if (userBean != null) {
            sb.append(", ").append(userBean);
        }
    }

    // ===================================================================================
    //                                                                       Table Command
    //                                                                       =============
    protected void setupTableCommandExp(StringBuilder sb, RomanticTransaction tx) {
        final Map<String, Set<String>> tableCommandMap = tx.getReadOnlyTableCommandMap();
        if (!tableCommandMap.isEmpty()) {
            final StringBuilder mapSb = new StringBuilder();
            mapSb.append("map:{");
            int index = 0;
            for (Entry<String, Set<String>> entry : tableCommandMap.entrySet()) {
                final String tableName = entry.getKey();
                final Set<String> commandSet = entry.getValue();
                if (index > 0) {
                    mapSb.append(" ; ");
                }
                mapSb.append(tableName);
                mapSb.append(" = list:{").append(Srl.connectByDelimiter(commandSet, " ; ")).append("}");
                ++index;
            }
            mapSb.append("}");
            sb.append(", ").append(mapSb.toString());
        }
    }

    // ===================================================================================
    //                                                                        Request Path
    //                                                                        ============
    protected void setupRequestPathExp(StringBuilder sb, RomanticTransaction tx) {
        final String requestPath = tx.getRequestPath();
        if (requestPath != null) {
            sb.append(", ").append(requestPath);
        }
    }

    // ===================================================================================
    //                                                                         Current SQL
    //                                                                         ===========
    protected void setupCurrentSqlExp(StringBuilder sb, RomanticTransaction tx) {
        final TransactionCurrentSqlBuilder currentSqlBuilder = tx.getCurrentSqlBuilder();
        if (currentSqlBuilder != null) {
            final String currentSql = currentSqlBuilder.buildSql();
            sb.append("\n/- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
            sb.append(" (SQL now: ");
            sb.append(tx.getCurrentTableName()).append("@").append(tx.getCurrentCommand());
            final Long currentSqlBeginMillis = tx.getCurrentSqlBeginMillis();
            if (currentSqlBeginMillis != null) {
                sb.append(" [").append(tx.buildElapsedTimeExp(currentSqlBeginMillis)).append("]");
            }
            sb.append(")\n");
            sb.append(currentSql);
            sb.append("\n- - - - - - - - - -/");
        }
    }
}