/*
 * The MIT License
 *
 * Copyright 2016 Ahseya.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.horrorho.inflatabledonkey.file;

import com.github.horrorho.inflatabledonkey.args.Property;
import com.github.horrorho.inflatabledonkey.chunk.Chunk;
import com.github.horrorho.inflatabledonkey.data.backup.Asset;
import com.github.horrorho.inflatabledonkey.io.DirectoryAssistant;
import com.github.horrorho.inflatabledonkey.io.IOFunction;
import com.github.horrorho.inflatabledonkey.io.IOSupplier;
import com.github.horrorho.inflatabledonkey.io.IOSupplierSequenceStream;
import com.github.horrorho.ragingmoose.LZFSEInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import javax.annotation.concurrent.Immutable;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * FileAssembler.
 *
 * @author Ahseya
 */
@Immutable
public final class FileAssembler
        implements BiConsumer<Asset, Optional<List<Chunk>>>, BiPredicate<Asset, Optional<List<Chunk>>> {

    private static final Logger logger = LoggerFactory.getLogger(FileAssembler.class);

    private static final boolean QUIET = Property.QUIET.asBoolean().orElse(false);

    private final Function<byte[], Optional<XFileKey>> fileKeys;
    private final UnaryOperator<Optional<XFileKey>> mutator;
    private final FilePath filePath;

    public FileAssembler(
            Function<byte[], Optional<XFileKey>> fileKeys,
            UnaryOperator<Optional<XFileKey>> mutator,
            FilePath filePath) {

        this.fileKeys = Objects.requireNonNull(fileKeys, "fileKeys");
        this.mutator = Objects.requireNonNull(mutator, "mutator");
        this.filePath = Objects.requireNonNull(filePath, "filePath");
    }

    public FileAssembler(Function<byte[], Optional<XFileKey>> fileKeys, Path outputFolder) {
        this(fileKeys, XFileKeyMutatorFactory.defaults(), new FilePath(outputFolder));
    }

    @Override
    public void accept(Asset asset, Optional<List<Chunk>> chunks) {
        boolean test = test(asset, chunks);
        if (!test) {
            logger.debug("-- accept() - failed to write asset: {}", asset.relativePath());
        }
    }

    @Override
    public boolean test(Asset asset, Optional<List<Chunk>> chunks) {
        logger.trace("<< test() - asset: {} chunks: {}",
                asset, chunks.map(List::size).map(Object::toString).orElse("NULL"));

        boolean success = chunks.isPresent()
                ? assemble(asset, chunks.get())
                : fail(asset);

        logger.trace(">> test() - success: {}", success);
        return success;
    }

    boolean fail(Asset asset) {
        logger.debug("-- fail() - failed: {}" + info(asset));
        if (!QUIET) {
            System.out.println("-- " + info(asset) + " failed");
        }
        return false;
    }

    boolean assemble(Asset asset, List<Chunk> chunks) {
        return filePath.apply(asset)
                .filter(DirectoryAssistant::createParent)
                .filter(path -> assemble(path, asset, chunks))
                .filter(path -> FileTruncater.truncate(path, asset))
                .map(path -> FileTimestamp.set(path, asset))
                .orElse(false);
    }

    boolean assemble(Path path, Asset asset, List<Chunk> chunks) {
        String info = info(asset);
        asset.contentCompressionMethod()
                .ifPresent(u -> logger.info("-- assemble() - asset: {} content compression method: {}", info, u));
        asset.contentEncodingMethod()
                .ifPresent(u -> logger.info("-- assemble() - asset: {} content encoding method: {}", info, u));
        return asset.encryptionKey()
                .map(u -> decrypt(path, info, chunks, u, asset.fileChecksum(), asset.contentCompressionMethod()))
                .orElseGet(() -> write(path, info, chunks, Optional.empty(), asset.fileChecksum(), asset.contentCompressionMethod()));
    }

    boolean decrypt(Path path,
            String info,
            List<Chunk> chunks,
            byte[] encryptionKey,
            Optional<byte[]> signature,
            Optional<Integer> compression) {
        return fileKeys.apply(encryptionKey)
                .map(Optional::of)
                .map(mutator)
                .map(u -> write(path, info, chunks, u, signature, compression))
                .orElseGet(() -> {
                    logger.warn("-- decrypt() - failed to unwrap encryption key");
                    return false;
                });
    }

    boolean write(Path path,
            String info,
            List<Chunk> chunks, Optional<XFileKey> keyCipher,
            Optional<byte[]> signature,
            Optional<Integer> compression) {
        logger.debug("-- write() - path: {} key cipher: {} signature: 0x{}",
                path, keyCipher, signature.map(Hex::toHexString).orElse("NULL"));

        boolean status = true;
        Optional<IOFunction<InputStream, InputStream>> decompress;
        if (compression.isPresent()) {
            if (compression.get() == 2) {
                decompress = Optional.of(LZFSEInputStream::new);
            } else {
                logger.warn("-- write() - unsupported compression: {} -> {}", info, compression.get());
                decompress = Optional.empty();
            }
        } else {
            decompress = Optional.empty();
        }

        try (OutputStream out = Files.newOutputStream(path);
                InputStream in = chunkStream(chunks)) {
            status &= FileStreamWriter.copy(in, out, keyCipher, signature, decompress);

            if (keyCipher.isPresent()) {
                XFileKey kc = keyCipher.get();
                logger.debug("-- write() - written: {} status: {} mode: {} flags: 0x{}",
                        path, status, kc.ciphers(), Hex.toHexString(kc.flags()));
                if (!QUIET) {
                    System.out.println(">> " + info + " " + kc.ciphers() + " " + Hex.toHexString(kc.flags()));
                }
            } else {
                logger.debug("-- write() - written: {} status: {}", path, status);
                if (!QUIET) {
                    System.out.println(">> " + info);
                }
            }
            return status;

        } catch (IOException | DataLengthException | IllegalStateException ex) {
            logger.warn("-- write() - error: ", ex);
            return false;
        }
    }

    InputStream chunkStream(List<Chunk> chunks) throws IOException {
        // Changed from java.io.SequenceInputStream which required open InputStreams as this was causing 'Too many open 
        // files' exceptions on assets with huge numbers of chunks.
        List<IOSupplier<InputStream>> suppliers = new ArrayList<>();
        for (Chunk chunk : chunks) {
            IOSupplier<InputStream> ios = () -> chunk.inputStream()
                    .orElseThrow(()
                            -> new IllegalStateException("chunk deleted: 0x" + Hex.toHexString(chunk.checksum())));
            suppliers.add(ios);
        }
        return new IOSupplierSequenceStream(suppliers);
    }

    String info(Asset asset) {
        return asset.domain().orElse("") + " " + asset.relativePath().orElse("");
    }

    @Override
    public String toString() {
        return "FileAssembler{" + "fileKeys=" + fileKeys + ", mutator=" + mutator + ", filePath=" + filePath + '}';
    }
}