/*
 *  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.synapse.transport.http.access;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.params.HttpParams;
import org.apache.synapse.transport.http.wrapper.HttpRequestWrapper;
import org.apache.synapse.transport.http.wrapper.HttpResponseWrapper;

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * The class to handle the HTTP Access Logs, patterns and the major functionality.
 * Major Code segment borrowed from Apache Tomcat's
 * org.apache.catalina.valves.AccessLogValve with thanks.
 */
public class Access {
    private static Log log = LogFactory.getLog(Access.class);

    /**
     * Array of AccessLogElement, they will be used to make log message.
     */
    protected AccessLogElement[] logElements = null;

    protected String pattern = AccessConstants.getLogPattern();

    private static AccessLogger accessLogger;

    private static ConcurrentLinkedQueue<HttpRequestWrapper> requestQueue;
    private static ConcurrentLinkedQueue<HttpResponseWrapper> responseQueue;

    private static final int LOG_FREQUENCY_IN_SECONDS = 30;

    private Date date;

    /**
     * Constructor of AccessLog. AccessHandler has a static object of Access.
     *
     * @param log          - Log passed as a param. Default is Log of the same class.
     * @param accessLogger - AccessLogger Object
     */
    public Access(final Log log, AccessLogger accessLogger) {
        super();
        Access.log = log;
        Access.accessLogger = accessLogger;
        requestQueue = new ConcurrentLinkedQueue<HttpRequestWrapper>();
        responseQueue = new ConcurrentLinkedQueue<HttpResponseWrapper>();
        logElements = createLogElements();
        logAccesses();
    }

    /**
     * Adds the accesses to the queue.
     *
     * @param request - HttpRequest
     */
    public void addAccessToQueue(HttpRequest request) {
        HttpRequestWrapper requestWrapper = new HttpRequestWrapper();
        requestWrapper.setHttpRequest(request);
        requestWrapper.setDate(new Date(AccessTimeUtil.getDate().getTime()));
        requestQueue.add(requestWrapper);
    }

    /**
     * Adds the accesses to the queue.
     *
     * @param response - HttpResponse
     */
    public void addAccessToQueue(HttpResponse response) {
        HttpResponseWrapper responseWrapper = new HttpResponseWrapper();
        responseWrapper.setHttpResponse(response);
        responseWrapper.setDate(new Date(AccessTimeUtil.getDate().getTime()));
        responseQueue.add(responseWrapper);
    }

    /**
     * logs the request and response accesses.
     */
    public void logAccesses() {
        TimerTask logRequests = new LogRequests();
        TimerTask logResponses = new LogResponses();
        Timer requestTimer = new Timer();
        Timer responseTimer = new Timer();
        // Retry in 30 seconds
        long retryIn = 1000 * LOG_FREQUENCY_IN_SECONDS;
        requestTimer.schedule(logRequests, 0, retryIn);
        responseTimer.schedule(logResponses, 0, retryIn);
    }

    private class LogRequests extends TimerTask {
        public void run() {
            while (!requestQueue.isEmpty()) {
                HttpRequestWrapper req = requestQueue.poll();
                log(req, null);
            }
        }
    }

    private class LogResponses extends TimerTask {
        public void run() {
            while (!responseQueue.isEmpty()) {
                HttpResponseWrapper res = responseQueue.poll();
                log(null, res);
            }
        }
    }

    /**
     * The log method that is called from the NHttpClient and Server connection classes.
     *
     * @param request  - HttpRequest
     * @param response - HttpResponse
     */
    public void log(HttpRequest request, HttpResponse response) {
        date = AccessTimeUtil.getDate();
        //String commonLogFormatDate = AccessTimeUtil.getAccessDate(date);

        StringBuilder result = new StringBuilder(128);

        for (AccessLogElement logElement : logElements) {
            logElement.addElement(result, date, request, response);
        }
        String logString = result.toString();
        log.debug(logString);      //log to the console
        accessLogger.log(logString);      //log to the file
    }

    /**
     * gets the header values from the given message, with the given name.
     *
     * @param message - The message, HttpRequest or HttpResponse
     * @param name    - The header, which we need to get the value of.
     * @return - The header value.
     */
    protected static String getHeaderValues(HttpMessage message, String name) {
        int length = 0;
        Header[] header = new Header[0];
        StringBuffer headerValue = new StringBuffer();
        try {
            header = message.getHeaders(name);
            length = header.length;
        } catch (Exception e) {
            // The header doesn't exist
        }
        if (length == 0) {
            return "-";
        } else if (length == 1) {
            return header[0].getValue();
        } else {
            headerValue.append(header[0].getValue());
            for (int i = 1; i < length; i++) {
                headerValue.append(" - ").append(header[i].getValue());
            }
        }
        return headerValue.toString();
    }

    protected static String getParam(HttpMessage message, String paramName) {
        HttpParams params = message.getParams();
        String param = (String) params.getParameter(paramName);
        if (param == null) {
            param = "-";
        }

        return param;
    }

    public static String getHostElement(HttpMessage message) {
        return getHeaderValues(message, "Host"); //%h;
    }

    public static String getLogicalUserNameElement(HttpMessage message) {
        if (message != null) {  //%l
            return "-";
        }
        return "";
    }

    public static String getUserNameElement(HttpMessage message) {
        return getHeaderValues(message, "From");     //%u
    }

    public static String getCookieElement(HttpMessage message) {
        return getHeaderValues(message, "Cookie");      // %c
    }

    public static String getRefererElement(HttpMessage message) {
        return getHeaderValues(message, "Referer"); //%{Referer}i;           %f
    }

    public static String getUserAgentElement(HttpMessage message) {
        return getHeaderValues(message, "User-Agent");           //%{User-Agent} %a
    }

    public static String getAcceptElement(HttpMessage message) {
        return getHeaderValues(message, "Accept");
    }

    public static String getAcceptLanguageElement(HttpMessage message) {
        return getHeaderValues(message, "Accept-Language");
    }

    public static String getAcceptEncodingElement(HttpMessage message) {
        return getHeaderValues(message, "Accept-Encoding");
    }

    public static String getAcceptCharSetElement(HttpMessage message) {
        return getHeaderValues(message, "Accept-Charset");
    }

    public static String getConnectionElement(HttpMessage message) {
        return getHeaderValues(message, "Connection");       //Keep-Alive
    }

    public static String getContentTypeElement(HttpMessage message) {
        return getHeaderValues(message, "Content-Type");
    }

    public static String getKeepAliveElement(HttpMessage message) {
        return getHeaderValues(message, "Keep-Alive");
    }

    public static String getTransferEncodingElement(HttpMessage message) {
        return getHeaderValues(message, "Transfer-Encoding");
    }

    public static String getContentEncodingElement(HttpMessage message) {
        return getHeaderValues(message, "Content-Encoding");
    }

    public static String getVaryElement(HttpMessage message) {
        return getHeaderValues(message, "Vary");
    }

    public static String getServerElement(HttpMessage message) {
        return getHeaderValues(message, "Server");
    }

    public static String getRemoteAddr(HttpMessage message) {
        return getParam(message, "http.remote.addr");
    }

    /**
     * AccessLogElement writes the partial message into the buffer.
     */
    protected interface AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response);
    }

    /**
     * write local IP address - %A
     */
    protected static class LocalAddrElement implements AccessLogElement {

        private static final String LOCAL_ADDR_VALUE;

        static {
            String init;
            try {
                init = InetAddress.getLocalHost().getHostAddress();
            } catch (Throwable e) {
                AccessTimeUtil.handleThrowable(e);
                init = "127.0.0.1";
            }
            LOCAL_ADDR_VALUE = init;
        }

        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(LOCAL_ADDR_VALUE);
        }
    }


    /**
     * write remote host name - %h
     */
    protected static class HostElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            String remoteHost = "-";
            try {
                if (request != null) {
                    remoteHost = getRemoteAddr(request);
                } else if (response != null) {
                    remoteHost = getRemoteAddr(response);
                }
            } catch (Exception e) {
                // empty host
            }
            buf.append(remoteHost);
        }

    }

    /**
     * write remote logical username from identd (always returns '-') - %l
     */
    protected static class LogicalUserNameElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            String logicalUserName = "";
            try {
                logicalUserName = getLogicalUserNameElement(request);
            } catch (Exception e) {
                // empty logicalUserName
            }
            buf.append(logicalUserName);
        }
    }


    /**
     * write remote user that was authenticated (if any), else '-' - %u
     */
    protected static class UserElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            String userElement = "";
            try {
                userElement = getUserNameElement(request);
            } catch (Exception e) {
                // empty UserName
            }
            buf.append(userElement);
        }
    }


    /**
     * write date and time, in Common Log Format - %t
     */
    protected class DateAndTimeElement implements AccessLogElement {

        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {

            String currentDate = AccessTimeUtil.getAccessDate(date);
            buf.append(currentDate);
        }
    }

    /**
     * write first line of the request (method and request URI) - %r
     */
    protected static class RequestElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            if (request != null) {
                String requestLine = request.getRequestLine().toString();
                buf.append(requestLine);
            } else {
                buf.append("- - ");
            }
        }
    }

    /**
     * write HTTP status code of the response - %s
     */
    protected static class HttpStatusCodeElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            if (response != null) {
                int statusCode = response.getStatusLine().getStatusCode();
                buf.append(statusCode); //getStatus
            } else {
                buf.append('-');
            }
        }
    }


    /**
     * write bytes sent, excluding HTTP headers - %b, %B
     */
    protected static class ByteSentElement implements AccessLogElement {
        private boolean conversion;

        /**
         * if conversion is true, write '-' instead of 0 - %b
         *
         * @param conversion - To be conversed.
         */
        public ByteSentElement(boolean conversion) {
            this.conversion = conversion;
        }

        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            // Don't need to flush since trigger for log message is after the
            // response has been committed
            long length = -1;
            try {
                if (request != null) {
                    String sLength = getHeaderValues(request, "Content-Length");
                    if(null != sLength && !"".equals(sLength)) {
                        length = Long.valueOf(sLength);
                    }
                } else if (response != null) {
                    length = response.getEntity().getContentLength(); //getBytesWritten(false);
                }

                if (length <= 0 && conversion) {
                    buf.append('-');                           //%b
                } else {
                    buf.append(length);
                }
            } catch (Exception e) {
                buf.append('-'); //No entity found.
            }
        }
    }

    /**
     * write request method (GET, POST, etc.) - %m
     */
    protected static class MethodElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            if (request != null) {
                buf.append(request.getRequestLine().getMethod());
            }
        }
    }

    /**
     * write requested URL path - %U
     */
    protected static class RequestURIElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            if (request != null) {
                buf.append(request.getRequestLine().getUri());
            } else {
                buf.append('-');
            }
        }
    }

    /**
     * write local server name - %v
     */
    protected static class LocalServerNameElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getHeaderValues(request, "server"));
        }
    }

    /**
     * write any string
     */
    protected static class StringElement implements AccessLogElement {
        private String str;

        public StringElement(String str) {
            this.str = str;
        }

        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(str);
        }
    }

    /**
     * write incoming headers - %{xxx}i
     */
    protected static class HeaderElement implements AccessLogElement {
        private String header;

        public HeaderElement(String header) {
            this.header = header;
        }

        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            try {
                String value = getHeaderValues(request, header);
                if (value == null) {
                    buf.append('-');
                } else {
                    buf.append(value);
                }
            } catch (Exception e) {
                buf.append('-'); //Header is null
            }
        }
    }

    /**
     * write a specific cookie - %{xxx}c
     */
    protected static class CookieElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getCookieElement(request));
        }
    }


    /**
     * write the referer - %f
     */
    protected static class RefererElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getRefererElement(request));
        }
    }


    /**
     * write the user agent - %a
     */
    protected static class UserAgentElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getUserAgentElement(request));
        }
    }

    /**
     * write the Accept Element - %C
     */
    protected static class AcceptElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getAcceptElement(request));
        }
    }

    /**
     * write the Accept Language Element - %L
     */
    protected static class AcceptLanguageElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getAcceptLanguageElement(request));
        }
    }

    /**
     * write the Accept Encoding Element - %e
     */
    protected static class AcceptEncodingElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getAcceptEncodingElement(request));
        }
    }

    /**
     * write the Accept Character Set Element - %S
     */
    protected static class AcceptCharSetElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getAcceptCharSetElement(request));
        }
    }

    /**
     * write the Connection Element - %x
     */
    protected static class ConnectionElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getConnectionElement(request));
        }
    }

    /**
     * write the Content Type Element - %T
     */
    protected static class ContentTypeElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getContentTypeElement(request));
        }
    }

    /**
     * write the Keep Alive Element - %k
     */
    protected static class KeepAliveElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getKeepAliveElement(request));
        }
    }

    /**
     * write the Transfer Encoding Element - %E
     */
    protected static class TransferEncodingElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getTransferEncodingElement(request));
        }
    }

    /**
     * write the Content Encoding Element - %n
     */
    protected static class ContentEncodingElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getContentEncodingElement(request));
        }
    }

    /**
     * write the Vary Element - %V
     */
    protected static class VaryElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getVaryElement(request));
        }
    }

    /**
     * write the Server Element - %Z
     */
    protected static class ServerElement implements AccessLogElement {
        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            buf.append(getServerElement(request));
        }
    }

    /**
     * write a specific response header - %{xxx}o
     */
    protected static class ResponseHeaderElement implements AccessLogElement {
        private String header;

        public ResponseHeaderElement(String header) {
            this.header = header;
        }

        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            if (null != response) {
                buf.append(getHeaderValues(response, header));
            }
            buf.append("-");
        }
    }

    /**
     * write an attribute in the ServletRequest - %{xxx}r    %R
     */
    protected static class RequestAttributeElement implements AccessLogElement {

        public RequestAttributeElement() {
        }

        public void addElement(StringBuilder buf, Date date, HttpRequest request,
                               HttpResponse response) {
            Object value;
            if (request != null) {
                value = request.getLastHeader(buf.toString()); //gets the attribute header
            } else {
                value = "??";
            }
            if (value != null) {
                if (value instanceof String) {
                    buf.append((String) value);
                } else {
                    buf.append(value.toString());
                }
            } else {
                buf.append('-');
            }
        }

    }


    /**
     * parse pattern string and create the array of AccessLogElement
     *
     * @return Array of AccessLogElement
     */
    protected AccessLogElement[] createLogElements() {
        List<AccessLogElement> list = new ArrayList<AccessLogElement>();
        boolean replace = false;
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < pattern.length(); i++) {
            char ch = pattern.charAt(i);
            if (replace) {
                /*
                 * For code that processes {, the behavior will be ... if I do
                 * not encounter a closing } - then I ignore the {
                 */
                if ('{' == ch) {
                    StringBuilder name = new StringBuilder();
                    int j = i + 1;
                    for (; j < pattern.length() && '}' != pattern.charAt(j); j++) {
                        name.append(pattern.charAt(j));
                    }
                    if (j + 1 < pattern.length()) {
                        /* the +1 was to account for } which we increment now */
                        j++;
                        list.add(createAccessLogElement(name.toString(),
                                                        pattern.charAt(j)));
                        i = j; /* Since we walked more than one character */
                    } else {
                        // D'oh - end of string - pretend we never did this
                        // and do processing the "old way"
                        list.add(createAccessLogElement(ch));
                    }
                } else {
                    list.add(createAccessLogElement(ch));
                }
                replace = false;
            } else if (ch == '%') {
                replace = true;
                list.add(new StringElement(buf.toString()));
                buf = new StringBuilder();
            } else {
                buf.append(ch);
            }
        }
        if (buf.length() > 0) {
            list.add(new StringElement(buf.toString()));
        }
        return list.toArray(new AccessLogElement[list.size()]);
    }

    /**
     * create an AccessLogElement implementation which needs header string
     *
     * @param header  - header of the request/response
     * @param pattern - pattern character given for the input element
     * @return AccessLogElement - accessLogElement
     */
    private AccessLogElement createAccessLogElement(String header, char pattern) {
        switch (pattern) {
            case 'i':
                return new HeaderElement(header);  //%{xxx}i
            case 'o':
                return new ResponseHeaderElement(header);
            case 'R':
                return new RequestAttributeElement();
            default:
                return new StringElement("???");
        }
    }

    /**
     * create an AccessLogElement implementation
     *
     * @param pattern - pattern character given for the input element
     * @return AccessLogElement acceessLogElement
     */
    private AccessLogElement createAccessLogElement(char pattern) {
        switch (pattern) {
            case 'A':
                return new LocalAddrElement();
            case 'a':
                return new UserAgentElement();
            case 'b':
                return new ByteSentElement(true);     //%b
            case 'B':
                return new ByteSentElement(false);
            case 'c':
                return new CookieElement();            // %c
            case 'C':
                return new AcceptElement();
            case 'e':
                return new AcceptEncodingElement();
            case 'E':
                return new TransferEncodingElement();
            case 'f':
                return new RefererElement();
            case 'h':
                return new HostElement();         //%h
            case 'k':
                return new KeepAliveElement();
            case 'l':
                return new LogicalUserNameElement();     //%l
            case 'L':
                return new AcceptLanguageElement();
            case 'm':
                return new MethodElement();
            case 'n':
                return new ContentEncodingElement();
            case 'r':
                return new RequestElement();        //%r
            case 'S':
                return new AcceptCharSetElement();
            case 's':
                return new HttpStatusCodeElement();       // %s
            case 'T':
                return new ContentTypeElement();
            case 't':
                return new DateAndTimeElement();       //%t
            case 'u':
                return new UserElement();           //%u
            case 'U':
                return new RequestURIElement();
            case 'V':
                return new VaryElement();
            case 'v':
                return new LocalServerNameElement();
            case 'x':
                return new ConnectionElement();
            case 'Z':
                return new ServerElement();
            default:
                return new StringElement("???" + pattern + "???");
        }
    }

    /**
     * The log method that is called from the NHttpClient and Server connection classes.
     * This method is used to log the request/response with the actual date of operation.
     *
     * @param request  - HttpRequestWrapper
     * @param response - HttpResponseWrapper
     */
    public void log(HttpRequestWrapper request, HttpResponseWrapper response) {

        Date actualDateOfOperation = null;

        if(request != null) {
            actualDateOfOperation = request.getDate();
        } else if (response != null){
            actualDateOfOperation = response.getDate();
        }

        StringBuilder result = new StringBuilder(128);

        for (AccessLogElement logElement : logElements) {
            if (request != null)    {
                logElement.addElement(result, actualDateOfOperation, request.getHttpRequest(), null);
            } else if (response != null)    {
                logElement.addElement(result, actualDateOfOperation, null, response.getHttpResponse());
            }
        }
        String logString = result.toString();
        log.debug(logString);      //log to the console
        accessLogger.log(logString);      //log to the file
    }
}
