/*******************************************************************************
 * 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.ofbiz.base.util;


import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.imaging.ImageReadException;
import org.apache.ofbiz.entity.Delegator;

/**
 * HttpRequestFileUpload - Receive a file upload through an HttpServletRequest
 *
 */
public class HttpRequestFileUpload {
    private static final String MODULE = HttpRequestFileUpload.class.getName();
    private static final int BUFFER_SIZE = 4096;
    private static final int WAIT_INTERVAL = 200; // in milliseconds
    private static final int MAX_WAITS = 20;
    private int waitCount = 0;
    private String savePath;
    private String filepath;
    private String filename;
    private String contentType;
    private String overrideFilename = null;
    private Map<String, String> fields;

    /**
     * Gets override filename.
     * @return the override filename
     */
    public String getOverrideFilename() {
        return overrideFilename;
    }

    /**
     * Sets override filename.
     * @param ofName the of name
     */
    public void setOverrideFilename(String ofName) {
        overrideFilename = ofName;
    }

    /**
     * Gets filename.
     * @return the filename
     */
    public String getFilename() {
        return filename;
    }

    /**
     * Gets filepath.
     * @return the filepath
     */
    public String getFilepath() {
        return filepath;
    }

    /**
     * Sets save path.
     * @param savePath the save path
     */
    public void setSavePath(String savePath) {
        this.savePath = savePath;
    }

    /**
     * Gets content type.
     * @return the content type
     */
    public String getContentType() {
        return contentType;
    }

    /**
     * Gets field value.
     * @param fieldName the field name
     * @return the field value
     */
    public String getFieldValue(String fieldName) {
        if (fields == null || fieldName == null) {
            return null;
        }
        return fields.get(fieldName);
    }

    private void setFilename(String s) {
        if (s == null) {
            return;
        }

        int pos = s.indexOf("filename=\"");

        if (pos != -1) {
            filepath = s.substring(pos + 10, s.length() - 1);
            // Windows browsers include the full path on the client
            // But Linux/Unix and Mac browsers only send the filename
            // test if this is from a Windows browser
            pos = filepath.lastIndexOf("\\");
            if (pos != -1) {
                filename = filepath.substring(pos + 1);
            } else {
                filename = filepath;
            }
        }
    }

    private void setContentType(String s) {
        if (s == null) {
            return;
        }

        int pos = s.indexOf(": ");

        if (pos != -1) {
            contentType = s.substring(pos + 2, s.length());
        }
    }

    /**
     * Do upload.
     * @param request the request
     * @throws IOException the io exception
     */
    public boolean doUpload(HttpServletRequest request, String fileType) throws IOException {
        Delegator delegator = (Delegator) request.getAttribute("delegator");
        ServletInputStream in = request.getInputStream();

        String reqLengthString = request.getHeader("content-length");
        Debug.logInfo("expect " + reqLengthString + " bytes.", MODULE);
        int requestLength = 0;

        try {
            requestLength = Integer.parseInt(reqLengthString);
        } catch (NumberFormatException e) {
            Debug.logError(e, e.getMessage(), MODULE);
            return false;
        }
        byte[] line = new byte[BUFFER_SIZE];

        int i = -1;

        i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength);
        requestLength -= i;
        if (i < 3) {
            Debug.logError("Possibly a waitingReadLine error", MODULE);
            return false;
        }
        int boundaryLength = i - 2;

        String boundary = new String(line, 0, boundaryLength, StandardCharsets.UTF_8); // -2 discards the newline character

        Debug.logInfo("boundary=[" + boundary + "] length is " + boundaryLength, MODULE);
        fields = new HashMap<>();

        while (requestLength > 0/* i != -1*/) {
            String newLine = "";

            if (i > -1) {
                newLine = new String(line, 0, i, StandardCharsets.UTF_8);
            }
            if (newLine.startsWith("Content-Disposition: form-data; name=\"")) {
                if (newLine.indexOf("filename=\"") != -1) {
                    setFilename(new String(line, 0, i - 2, StandardCharsets.UTF_8));
                    if (filename == null) {
                        return false;
                    }
                    // this is the file content
                    i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength);
                    requestLength -= i;

                    setContentType(new String(line, 0, i - 2, StandardCharsets.UTF_8));

                    // blank line
                    i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength);
                    requestLength -= i;
                    String filenameToUse = filename;

                    if (overrideFilename != null) {
                        filenameToUse = overrideFilename;
                    }

                    // first line of actual file
                    i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength);
                    requestLength -= i;
                    byte[] lastTwoBytes = new byte[2];

                    if (i > 1) {
                        lastTwoBytes[0] = line[i - 2];
                        lastTwoBytes[1] = line[i - 1];
                    }
                    Debug.logInfo("about to create a file:" + (savePath == null ? "" : savePath) + filenameToUse, MODULE);
                    // before creating the file make sure directory exists
                    if (savePath == null) {
                        throw new IllegalArgumentException("savePath is null");
                    }
                    File savePathFile = new File(savePath);
                    if (!savePathFile.exists()) {
                        if (!savePathFile.mkdirs()) {
                            Debug.logError("Directory could not be created", filenameToUse);
                        }

                    }
                    String fileTocheck = savePath + filenameToUse;
                    try (FileOutputStream fos = new FileOutputStream(fileTocheck);) {
                        boolean bail = (new String(line, 0, i, StandardCharsets.UTF_8).startsWith(boundary));
                        boolean oneByteLine = (i == 1); // handle one-byte lines

                        while ((requestLength > 0/* i != -1 */) && !bail) {

                            // write the current buffer, except the last 2 bytes;
                            if (i > 1) {
                                fos.write(line, 0, i - 2);
                            }

                            oneByteLine = (i == 1); // we need to track on-byte lines differently

                            i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength);
                            requestLength -= i;

                            // the problem is the last line of the file content
                            // contains the new line character.

                            // if the line just read was the last line, we're done.
                            // if not, we must write the last 2 bytes of the previous buffer
                            // just assume that a one-byte line isn't the last line

                            if (requestLength < 1) {
                                bail = true;
                            } else if (oneByteLine) {
                                fos.write(lastTwoBytes, 0, 1); // we only saved one byte
                            } else {
                                fos.write(lastTwoBytes, 0, 2);
                            }

                            if (i > 1) {
                                // save the last 2 bytes of the buffer
                                lastTwoBytes[0] = line[i - 2];
                                lastTwoBytes[1] = line[i - 1];
                            } else {
                                lastTwoBytes[0] = line[0]; // only save one byte
                            }
                        }
                        fos.flush();

                        // Check if a webshell is not uploaded
                        if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(fileTocheck, fileType, delegator)) {
                            return false;
                        }
                    } catch (ImageReadException e) {
                        Debug.logError(e, MODULE);
                        return false;
                    }
                } else {
                    // this is a field
                    // get the field name
                    int pos = newLine.indexOf("name=\"");
                    String fieldName = newLine.substring(pos + 6, newLine.length() - 3);

                    // blank line
                    i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength);
                    requestLength -= i;
                    i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength);
                    requestLength -= i;
                    newLine = new String(line, 0, i, StandardCharsets.UTF_8);
                    StringBuilder fieldValue = new StringBuilder(BUFFER_SIZE);

                    while (requestLength > 0/* i != -1*/ && !newLine.startsWith(boundary)) {
                        // The last line of the field
                        // contains the new line character.
                        // So, we need to check if the current line is
                        // the last line.
                        i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength);
                        requestLength -= i;
                        if ((i == boundaryLength + 2 || i == boundaryLength + 4) // + 4 is eof
                                && (new String(line, 0, i).startsWith(boundary))) {
                            fieldValue.append(newLine.substring(0, newLine.length() - 2));
                        } else {
                            fieldValue.append(newLine);
                        }
                        newLine = new String(line, 0, i, StandardCharsets.UTF_8);
                    }
                    fields.put(fieldName, fieldValue.toString());
                }
            }
            i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength);
            if (i > -1) {
                requestLength -= i;
            }

        } // end while
        return true;
    }

    // reads a line, waiting if there is nothing available and reqLen > 0
    private int waitingReadLine(ServletInputStream in, byte[] buf, int off, int len, int reqLen) throws IOException {
        int i = -1;

        while (((i = in.readLine(buf, off, len)) == -1) && (reqLen > 0)) {
            Debug.logInfo("waiting", MODULE);
            if (waitCount > MAX_WAITS) {
                Debug.logInfo("waited " + waitCount + " times, bailing out while still expecting "
                        + reqLen + " bytes.", MODULE);
                throw new IOException("waited " + waitCount + " times, bailing out while still expecting "
                        + reqLen + " bytes.");
            }
            waitCount++;
            long endMS = new Date().getTime() + WAIT_INTERVAL;

            while (endMS > (new Date().getTime())) {
                try {
                    wait(WAIT_INTERVAL);
                } catch (InterruptedException e) {
                    Debug.logInfo(".", MODULE);
                }
            }
            Debug.logInfo((new Date().getTime() - endMS) + " ms", MODULE);
        }
        return i;
    }

}
