/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.dubbo.rpc.support;

import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.RpcContext;

import com.alibaba.fastjson.JSON;

import static org.apache.dubbo.common.constants.CommonConstants.GROUP_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.VERSION_KEY;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * AccessLogData is a container for log event data. In internally uses map and store each filed of log as value. It
 * does not generate any dynamic value e.g. time stamp, local jmv machine host address etc. It does not allow any null
 * or empty key.
 *
 * Note: since its date formatter is a singleton, make sure to run it in single thread only.
 */
public final class AccessLogData {

    private static final String MESSAGE_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    private static final DateFormat MESSAGE_DATE_FORMATTER = new SimpleDateFormat(MESSAGE_DATE_FORMAT);

    private static final String VERSION = "version";
    private static final String GROUP = "group";
    private static final String SERVICE = "service";
    private static final String METHOD_NAME = "method-name";
    private static final String INVOCATION_TIME = "invocation-time";
    private static final String TYPES = "types";
    private static final String ARGUMENTS = "arguments";
    private static final String REMOTE_HOST = "remote-host";
    private static final String REMOTE_PORT = "remote-port";
    private static final String LOCAL_HOST = "localhost";
    private static final String LOCAL_PORT = "local-port";

    /**
     * This is used to store log data in key val format.
     */
    private Map<String, Object> data;

    /**
     * Default constructor.
     */
    private AccessLogData() {
        RpcContext context = RpcContext.getContext();
        data = new HashMap<>();
        setLocalHost(context.getLocalHost());
        setLocalPort(context.getLocalPort());
        setRemoteHost(context.getRemoteHost());
        setRemotePort(context.getRemotePort());
    }

    /**
     * Get new instance of log data.
     *
     * @return instance of AccessLogData
     */
    public static AccessLogData newLogData() {
        return new AccessLogData();
    }


    /**
     * Add version information.
     *
     * @param version
     */
    public void setVersion(String version) {
        set(VERSION, version);
    }

    /**
     * Add service name.
     *
     * @param serviceName
     */
    public void setServiceName(String serviceName) {
        set(SERVICE, serviceName);
    }

    /**
     * Add group name
     *
     * @param group
     */
    public void setGroup(String group) {
        set(GROUP, group);
    }

    /**
     * Set the invocation date. As an argument it accept date string.
     *
     * @param invocationTime
     */
    public void setInvocationTime(Date invocationTime) {
        set(INVOCATION_TIME, invocationTime);
    }

    /**
     * Set caller remote host
     *
     * @param remoteHost
     */
    private void setRemoteHost(String remoteHost) {
        set(REMOTE_HOST, remoteHost);
    }

    /**
     * Set caller remote port.
     *
     * @param remotePort
     */
    private void setRemotePort(Integer remotePort) {
        set(REMOTE_PORT, remotePort);
    }

    /**
     * Set local host
     *
     * @param localHost
     */
    private void setLocalHost(String localHost) {
        set(LOCAL_HOST, localHost);
    }

    /**
     * Set local port of exported service
     *
     * @param localPort
     */
    private void setLocalPort(Integer localPort) {
        set(LOCAL_PORT, localPort);
    }

    /**
     * Set target method name.
     *
     * @param methodName
     */
    public void setMethodName(String methodName) {
        set(METHOD_NAME, methodName);
    }

    /**
     * Set invocation's method's input parameter's types
     *
     * @param types
     */
    public void setTypes(Class[] types) {
        set(TYPES, types != null ? Arrays.copyOf(types, types.length) : null);
    }

    /**
     * Sets invocation arguments
     *
     * @param arguments
     */
    public void setArguments(Object[] arguments) {
        set(ARGUMENTS, arguments != null ? Arrays.copyOf(arguments, arguments.length) : null);
    }

    /**
     * Return gthe service of access log entry
     *
     * @return
     */
    public String getServiceName() {
        return get(SERVICE).toString();
    }


    public String getLogMessage() {
        StringBuilder sn = new StringBuilder();

        sn.append("[")
                .append(MESSAGE_DATE_FORMATTER.format(getInvocationTime()))
                .append("] ")
                .append(get(REMOTE_HOST))
                .append(":")
                .append(get(REMOTE_PORT))
                .append(" -> ")
                .append(get(LOCAL_HOST))
                .append(":")
                .append(get(LOCAL_PORT))
                .append(" - ");

        String group = get(GROUP) != null ? get(GROUP).toString() : "";
        if (StringUtils.isNotEmpty(group)) {
            sn.append(group).append("/");
        }

        sn.append(get(SERVICE));

        String version = get(VERSION) != null ? get(VERSION).toString() : "";
        if (StringUtils.isNotEmpty(version)) {
            sn.append(":").append(version);
        }

        sn.append(" ");
        sn.append(get(METHOD_NAME));

        sn.append("(");
        Class<?>[] types = get(TYPES) != null ? (Class<?>[]) get(TYPES) : new Class[0];
        boolean first = true;
        for (Class<?> type : types) {
            if (first) {
                first = false;
            } else {
                sn.append(",");
            }
            sn.append(type.getName());
        }
        sn.append(") ");


        Object[] args = get(ARGUMENTS) != null ? (Object[]) get(ARGUMENTS) : null;
        if (args != null && args.length > 0) {
            sn.append(JSON.toJSONString(args));
        }

        return sn.toString();
    }

    private Date getInvocationTime() {
        return (Date)get(INVOCATION_TIME);
    }
    /**
     * Return value of key
     *
     * @param key
     * @return
     */
    private Object get(String key) {
        return data.get(key);
    }

    /**
     * Add log key along with his value.
     *
     * @param key   Any not null or non empty string
     * @param value Any object including null.
     */
    private void set(String key, Object value) {
        data.put(key, value);
    }
    
    public void buildAccessLogData(Invoker<?> invoker, Invocation inv) {
        setServiceName(invoker.getInterface().getName());
        setMethodName(inv.getMethodName());
        setVersion(invoker.getUrl().getParameter(VERSION_KEY));
        setGroup(invoker.getUrl().getParameter(GROUP_KEY));
        setInvocationTime(new Date());
        setTypes(inv.getParameterTypes());
        setArguments(inv.getArguments());
    }

}
