/*
 * Copyright 1999,2004 The Apache Software Foundation.
 *
 * 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.modeshape.webdav.methods;

import java.io.IOException;
import java.io.Writer;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.modeshape.common.logging.Logger;
import org.modeshape.common.util.StringUtil;
import org.modeshape.webdav.IMethodExecutor;
import org.modeshape.webdav.ITransaction;
import org.modeshape.webdav.StoredObject;
import org.modeshape.webdav.WebdavStatus;
import org.modeshape.webdav.exceptions.LockFailedException;
import org.modeshape.webdav.fromcatalina.URLEncoder;
import org.modeshape.webdav.fromcatalina.XMLWriter;
import org.modeshape.webdav.locking.IResourceLocks;
import org.modeshape.webdav.locking.LockedObject;

public abstract class AbstractMethod implements IMethodExecutor {
    
    
    private static final Pattern MULTI_LOCK_PATTERN = Pattern.compile("(.*<.*locktoken\\:.+\\>.*).*(<.*locktoken\\:.+\\>.*)");
    private static final Pattern SINGLE_LOCK_PATTERN = Pattern.compile("(.*<.*locktoken\\:.+\\>.*)");
    private static final Pattern LOCK_TOKEN_PATTERN = Pattern.compile("(.*<??.*locktoken:)([a-zA-z0-9\\-]+)(>??.*)");
    
    /**
     * Array containing the safe characters set.
     */
    protected static URLEncoder URL_ENCODER;

    /**
     * Default depth is infite.
     */
    protected static final int INFINITY = 3;

    /**
     * Simple date format for the creation date ISO 8601 representation (partial).
     */
    protected static final DateTimeFormatter CREATION_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
                                                                              .withZone(ZoneId.of("GMT")); 

    /**
     * Simple date format for the last modified date. (RFC 822 updated by RFC 1123)
     */
    protected static final DateTimeFormatter LAST_MODIFIED_DATE_FORMAT = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z",
                                                                                                     Locale.US).withZone(ZoneId.of("GMT")); 

    static {
        /**
         * GMT timezone - all HTTP dates are on GMT
         */
        URL_ENCODER = new URLEncoder();
        URL_ENCODER.addSafeCharacter('-');
        URL_ENCODER.addSafeCharacter('_');
        URL_ENCODER.addSafeCharacter('.');
        URL_ENCODER.addSafeCharacter('*');
        URL_ENCODER.addSafeCharacter('/');
    }

    /**
     * size of the io-buffer
     */
    protected static int BUF_SIZE = 65536;

    /**
     * Default lock timeout value.
     */
    protected static final int DEFAULT_TIMEOUT = 3600;

    /**
     * Maximum lock timeout.
     */
    protected static final int MAX_TIMEOUT = 604800;

    /**
     * Boolean value to temporary lock resources (for method locks)
     */
    protected static final boolean TEMPORARY = true;

    /**
     * Timeout for temporary locks
     */
    protected static final int TEMP_TIMEOUT = 10;

    protected Logger logger;

    protected AbstractMethod() {
        logger = Logger.getLogger(getClass());
    }

    public static String lastModifiedDateFormat( final Date date ) {
        return LAST_MODIFIED_DATE_FORMAT.format(date.toInstant());
    }

    public static String creationDateFormat( final Date date ) {
        return CREATION_DATE_FORMAT.format(date.toInstant());
    }

    /**
     * Return the relative path associated with this servlet.
     * 
     * @param request The servlet request we are processing
     * @return the relative path
     */
    protected String getRelativePath( HttpServletRequest request ) {

        // Are we being processed by a RequestDispatcher.include()?
        if (request.getAttribute("javax.servlet.include.request_uri") != null) {
            String result = (String)request.getAttribute("javax.servlet.include.path_info");
            // if (result == null)
            // result = (String) request
            // .getAttribute("javax.servlet.include.servlet_path");
            if ((result == null) || (result.equals(""))) {
                result = "/";
            }
            return result;
        }

        // No, extract the desired path directly from the request
        String result = request.getPathInfo();

        if ((result == null) || (result.equals(""))) {
            result = "/";
        }
        return result;
    }

    /**
     * creates the parent path from the given path by removing the last '/' and everything after that
     * 
     * @param path the path
     * @return parent path
     */
    protected String getParentPath( String path ) {
        int slash = path.lastIndexOf('/');
        if (slash != -1) {
            return path.substring(0, slash);
        }
        return null;
    }

    /**
     * removes a / at the end of the path string, if present
     * 
     * @param path the path
     * @return the path without trailing /
     */
    protected String getCleanPath( String path ) {

        if (path.endsWith("/") && path.length() > 1) {
            path = path.substring(0, path.length() - 1);
        }
        return path;
    }

    /**
     * Return JAXP document builder instance.
     * 
     * @return the builder
     * @throws ServletException
     */
    protected DocumentBuilder getDocumentBuilder() throws ServletException {
        DocumentBuilder documentBuilder = null;
        DocumentBuilderFactory documentBuilderFactory = null;
        try {
            documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            documentBuilder = documentBuilderFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new ServletException("jaxp failed");
        }
        return documentBuilder;
    }

    /**
     * reads the depth header from the request and returns it as a int
     * 
     * @param req
     * @return the depth from the depth header
     */
    protected int getDepth( HttpServletRequest req ) {
        int depth = INFINITY;
        String depthStr = req.getHeader("Depth");
        if (depthStr != null) {
            if (depthStr.equals("0")) {
                depth = 0;
            } else if (depthStr.equals("1")) {
                depth = 1;
            }
        }
        return depth;
    }

    /**
     * URL rewriter.
     * 
     * @param path Path which has to be rewiten
     * @return the rewritten path
     */
    protected String rewriteUrl( String path ) {
        return URL_ENCODER.encode(path);
    }

    /**
     * Get the ETag associated with a file.
     * 
     * @param so StoredObject to get resourceLength, lastModified and a hashCode of StoredObject
     * @return the ETag
     */
    protected String getETag( StoredObject so ) {

        String resourceLength = "";
        String lastModified = "";

        if (so != null && so.isResource()) {
            resourceLength = Long.toString(so.getResourceLength());
            lastModified = Long.toString(so.getLastModified().getTime());
        }

        return "W/\"" + resourceLength + "-" + lastModified + "\"";

    }

    protected String[] getLockIdFromIfHeader( HttpServletRequest req ) {
        String id = req.getHeader("If");
        if (id == null) {
            return null;
        }
        id = id.trim();
        if (id.length() == 0) {
            return null;
        }
        return lockTokensFrom(id.trim());
    }
    
    protected String[] lockTokensFrom(String id) {
        Matcher matcher = MULTI_LOCK_PATTERN.matcher(id);
        if (matcher.matches()) {
            return new String[] {lockIDFromToken(matcher.group(1)), lockIDFromToken(matcher.group(2))};
        }
        matcher = SINGLE_LOCK_PATTERN.matcher(id);
        if (matcher.matches()) {
            return new String[] {lockIDFromToken(matcher.group(1))};
        }
        return null;
    }
    
    protected String lockIDFromToken(String token) {
        Matcher matcher = LOCK_TOKEN_PATTERN.matcher(token);
        return matcher.matches() ? matcher.group(2) : token;
    }

    protected String getLockIdFromLockTokenHeader( HttpServletRequest req ) {
        String id = req.getHeader("Lock-Token");
        if (id == null || id.trim().length() == 0) {
            return id;
        }
        return lockIDFromToken(id);
    }

    /**
     * Checks if locks on resources at the given path exists and if so checks the If-Header to make sure the If-Header corresponds
     * to the locked resource. Returning true if no lock exists or the If-Header is corresponding to the locked resource
     * 
     * @param transaction
     * @param req Servlet request
     * @param resourceLocks
     * @param path path to the resource
     * @return true if no lock on a resource with the given path exists or if the If-Header corresponds to the locked resource
     * @throws IOException
     * @throws LockFailedException
     */
    protected boolean isUnlocked( ITransaction transaction,
                                  HttpServletRequest req,
                                  IResourceLocks resourceLocks,
                                  String path ) throws IOException, LockFailedException {

        LockedObject resourceLock = resourceLocks.getLockedObjectByPath(transaction, path);
        if (resourceLock == null || resourceLock.isShared()) {
            return true;
        }

        // the resource is locked
        String[] requestLockTokens = getLockIdFromIfHeader(req);
        String requestLockToken = null;
        if (requestLockTokens != null) {
            requestLockToken = requestLockTokens[0];
            LockedObject lockedObjectByToken = resourceLocks.getLockedObjectByID(transaction, requestLockToken);
            return lockedObjectByToken != null && lockedObjectByToken.equals(resourceLock);
        }
        return false;
    }

    /**
     * Send a multistatus element containing a complete error report to the client. If the errorList contains only one error, send
     * the error directly without wrapping it in a multistatus message.
     * 
     * @param req Servlet request
     * @param resp Servlet response
     * @param errorList List of error to be displayed
     * @throws IOException
     */
    protected void sendReport( HttpServletRequest req,
                               HttpServletResponse resp,
                               Hashtable<String, Integer> errorList ) throws IOException {

        if (errorList.size() == 1) {
            int code = errorList.elements().nextElement();
            if (!StringUtil.isBlank(WebdavStatus.getStatusText(code))) {
                resp.sendError(code, WebdavStatus.getStatusText(code));
            } else {
                resp.sendError(code);
            }
        } else {
            resp.setStatus(WebdavStatus.SC_MULTI_STATUS);

            String absoluteUri = req.getRequestURI();
            // String relativePath = getRelativePath(req);

            HashMap<String, String> namespaces = new HashMap<String, String>();
            namespaces.put("DAV:", "D");

            XMLWriter generatedXML = new XMLWriter(namespaces);
            generatedXML.writeXMLHeader();

            generatedXML.writeElement("DAV::multistatus", XMLWriter.OPENING);

            Enumeration<String> pathList = errorList.keys();
            while (pathList.hasMoreElements()) {

                String errorPath = pathList.nextElement();
                int errorCode = errorList.get(errorPath);

                generatedXML.writeElement("DAV::response", XMLWriter.OPENING);

                generatedXML.writeElement("DAV::href", XMLWriter.OPENING);
                String toAppend = null;
                if (absoluteUri.endsWith(errorPath)) {
                    toAppend = absoluteUri;

                } else if (absoluteUri.contains(errorPath)) {

                    int endIndex = absoluteUri.indexOf(errorPath) + errorPath.length();
                    toAppend = absoluteUri.substring(0, endIndex);
                }

                if (toAppend != null && !toAppend.startsWith("/") && !toAppend.startsWith("http:")) {
                    toAppend = "/" + toAppend;
                }
                generatedXML.writeText(errorPath);
                generatedXML.writeElement("DAV::href", XMLWriter.CLOSING);
                generatedXML.writeElement("DAV::status", XMLWriter.OPENING);
                generatedXML.writeText("HTTP/1.1 " + errorCode + " " + WebdavStatus.getStatusText(errorCode));
                generatedXML.writeElement("DAV::status", XMLWriter.CLOSING);

                generatedXML.writeElement("DAV::response", XMLWriter.CLOSING);

            }

            generatedXML.writeElement("DAV::multistatus", XMLWriter.CLOSING);

            Writer writer = resp.getWriter();
            writer.write(generatedXML.toString());
            writer.close();
        }
    }

}
