package io.tus.java.client;

import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * This class contains information about a file which will be uploaded later. Uploading is not
 * done using this class but using {@link TusUploader} whose instances are returned by
 * {@link TusClient#createUpload(TusUpload)}, {@link TusClient#createUpload(TusUpload)} and
 * {@link TusClient#resumeOrCreateUpload(TusUpload)}.
 */
public class TusUpload {
    private long size;
    private InputStream input;
    private TusInputStream tusInputStream;
    private String fingerprint;
    private Map<String, String> metadata;

    /**
     * Create a new TusUpload object.
     */
    public TusUpload() {
    }

    /**
     * Create a new TusUpload object using the supplied File object. The corresponding {@link
     * InputStream}, size and fingerprint will be automatically set.
     *
     * @param file The file whose content should be later uploaded.
     * @throws FileNotFoundException Thrown if the file cannot be found.
     */
    public TusUpload(@NotNull File file) throws FileNotFoundException {
        size = file.length();
        setInputStream(new FileInputStream(file));

        fingerprint = String.format("%s-%d", file.getAbsolutePath(), size);

        metadata = new HashMap<String, String>();
        metadata.put("filename", file.getName());
    }

    public long getSize() {
        return size;
    }

    /**
     * Set the file's size in bytes.
     *
     * @param size File's size in bytes.
     */
    public void setSize(long size) {
        this.size = size;
    }

    public String getFingerprint() {
        return fingerprint;
    }

    public void setFingerprint(String fingerprint) {
        this.fingerprint = fingerprint;
    }

    public InputStream getInputStream() {
        return input;
    }

    TusInputStream getTusInputStream() {
        return tusInputStream;
    }

    /**
     * Set the source from which will be read if the file will be later uploaded.
     *
     * @param inputStream The stream which will be read.
     */
    public void setInputStream(InputStream inputStream) {
        input = inputStream;
        tusInputStream = new TusInputStream(inputStream);
    }

    public void setMetadata(Map<String, String> metadata) {
        this.metadata = metadata;
    }

    public Map<String, String> getMetadata() {
        return metadata;
    }

    /**
     * Encode the metadata into a string according to the specification, so it can be
     * used as the value for the Upload-Metadata header.
     *
     * @return Encoded metadata
     */
    public String getEncodedMetadata() {
        if(metadata == null || metadata.size() == 0) {
            return "";
        }

        String encoded = "";

        boolean firstElement = true;
        for(Map.Entry<String, String> entry : metadata.entrySet()) {
            if(!firstElement) {
                encoded += ",";
            }
            encoded += entry.getKey() + " " + base64Encode(entry.getValue().getBytes());

            firstElement = false;
        }

        return encoded;
    }

    /**
     * Encode a byte-array using Base64. This is a sligtly modified version from an implementation
     * published on Wikipedia (https://en.wikipedia.org/wiki/Base64#Sample_Implementation_in_Java)
     * under the Creative Commons Attribution-ShareAlike License.
     */
    static String base64Encode(byte[] in)       {
        StringBuilder out = new StringBuilder((in.length * 4) / 3);
        String codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

        int b;
        for (int i = 0; i < in.length; i += 3)  {
            b = (in[i] & 0xFC) >> 2;
            out.append(codes.charAt(b));
            b = (in[i] & 0x03) << 4;
            if (i + 1 < in.length)      {
                b |= (in[i + 1] & 0xF0) >> 4;
                out.append(codes.charAt(b));
                b = (in[i + 1] & 0x0F) << 2;
                if (i + 2 < in.length)  {
                    b |= (in[i + 2] & 0xC0) >> 6;
                    out.append(codes.charAt(b));
                    b = in[i + 2] & 0x3F;
                    out.append(codes.charAt(b));
                } else  {
                    out.append(codes.charAt(b));
                    out.append('=');
                }
            } else      {
                out.append(codes.charAt(b));
                out.append("==");
            }
        }

        return out.toString();
    }
}