/*
 * AppleCommander - An Apple ][ image utility.
 * Copyright (C) 2019-2022 by Robert Greene and others
 * robgreene at users.sourceforge.net
 *
 * This program is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU General Public License as published by the 
 * Free Software Foundation; either version 2 of the License, or (at your 
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along 
 * with this program; if not, write to the Free Software Foundation, Inc., 
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package io.github.applecommander.acx.command;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import com.webcodepro.applecommander.storage.FormattedDisk;
import com.webcodepro.applecommander.storage.FormattedDisk.FileColumnHeader;
import com.webcodepro.applecommander.ui.DirectoryLister.CsvListingStrategy;
import com.webcodepro.applecommander.ui.DirectoryLister.JsonListingStrategy;
import com.webcodepro.applecommander.ui.DirectoryLister.ListingStrategy;
import com.webcodepro.applecommander.util.filestreamer.FileStreamer;
import com.webcodepro.applecommander.util.filestreamer.FileTuple;
import com.webcodepro.applecommander.util.filestreamer.TypeOfFile;

import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

@Command(name = "list", description = "List directory of disk image(s).",
        aliases = { "ls" })
public class ListCommand extends ReadOnlyDiskImageCommandOptions {
    @ArgGroup(exclusive = true, multiplicity = "0..1", heading = "%nFile display formatting:%n")
    private FileDisplay fileDisplay = new FileDisplay();
    
    @Option(names = { "-r", "--recursive"}, description = "Display directory recursively.", negatable = true, defaultValue = "false")
    private boolean recursiveFlag;
    
    @Option(names = { "--deleted" }, description = "Show deleted files.")
    private boolean deletedFlag;
    
    @ArgGroup(exclusive = true, multiplicity = "0..1")
    private TypeOfFileSelection typeOfFile = new TypeOfFileSelection();
    
    @ArgGroup(exclusive = true, multiplicity = "0..1", heading = "%nOutput format:%n")
    private OutputType outputType = new OutputType();

    @Option(names = "--header", negatable = true, description = "Show header.")
    private boolean headerFlag = true;

    @Option(names = "--column", negatable = true, description = "Show column headers.")
    private boolean columnFlag = true;

    @Option(names = "--footer", negatable = true, description = "Show footer.")
    private boolean footerFlag = true;

    @Option(names = "--globs", defaultValue = "*", split = ",", description = "File glob(s) to match.")
    private List<String> globs = new ArrayList<String>();

    @Override
    public int handleCommand() throws Exception {
        int display = fileDisplay.format();
        ListingStrategy listingStrategy = outputType.create(display);
        
        listingStrategy.first(disk);
        
        FileStreamer.forDisk(disk)
                    .ignoreErrors(true)
                    .includeDeleted(deletedFlag)
                    .recursive(recursiveFlag)
                    .includeTypeOfFile(typeOfFile.typeOfFile())
                    .matchGlobs(globs)
                    .beforeDisk(listingStrategy::beforeDisk)
                    .afterDisk(listingStrategy::afterDisk)
                    .stream()
                    .forEach(listingStrategy::forEach);
        
        listingStrategy.last(disk);
        
        return 0;
    }
    
    public static class FileDisplay {
        public int format() {
            if (standardFormat) {
                return FormattedDisk.FILE_DISPLAY_STANDARD;
            }
            if (longFormat) {
                return FormattedDisk.FILE_DISPLAY_DETAIL;
            }
            return FormattedDisk.FILE_DISPLAY_NATIVE;
        }
        
        @Option(names = { "-n", "--native" }, description = "Use native directory format (default).")
        private boolean nativeFormat;

        @Option(names = { "-s", "--short", "--standard" }, description = "Use brief directory format.")
        private boolean standardFormat;
        
        @Option(names = { "-l", "--long",  "--detail" }, description = "Use long/detailed directory format.")
        private boolean longFormat;
    }
    
    public static class OutputType {
        private OutputStrategy outputStrategy = OutputStrategy.TEXT;
        public ListingStrategy create(int display) {
            return outputStrategy.create(display);
        }

        private enum OutputStrategy { 
            TEXT(FormattedTextListingStrategy::new), 
            CSV(CsvListingStrategy::new),
            JSON(JsonListingStrategy::new);
            
            private Function<Integer,ListingStrategy> constructorFn;
            
            private OutputStrategy(Function<Integer,ListingStrategy> constructorFn) {
                this.constructorFn = constructorFn;
            }
            
            public ListingStrategy create(int display) {
                return constructorFn.apply(display);
            }

        };
        
        @Option(names = "--text", description = "Formatted text (default).")
        public void selectTextOutput(boolean flag) {
            this.outputStrategy = OutputStrategy.TEXT;
        }
        
        @Option(names = "--json", description = "JSON output.")
        public void selectJsonOutput(boolean flag) {
            this.outputStrategy = OutputStrategy.JSON;
        }
        
        @Option(names = "--csv", description = "CSV output.")
        public void selectCsvOutput(boolean flag) {
            this.outputStrategy = OutputStrategy.CSV;
        }
    }
    
    public static class FormattedTextListingStrategy extends ListingStrategy {
        private List<String> fmtSpec;
        
        public FormattedTextListingStrategy(int display) {
            super(display);
        }
        @Override
        public void beforeDisk(FormattedDisk disk) {
            List<FileColumnHeader> headers = disk.getFileColumnHeaders(display);
            fmtSpec = createFormatSpec(headers);
        
            System.out.println();
            System.out.printf("File: %s\n", disk.getFilename());
            System.out.printf("Name: %s\n", disk.getDiskName());
        }
        @Override
        public void forEach(FileTuple tuple) {
            List<String> data = tuple.fileEntry.getFileColumnData(display);
            for (int i=0; i<tuple.paths.size(); i++) { 
                System.out.print("  ");
            }
            for (int d = 0; d < data.size(); d++) {
                System.out.printf(fmtSpec.get(d), data.get(d));
            }
            if (tuple.fileEntry.isDeleted()) {
                System.out.print("[deleted]");
            }
            System.out.println();
        }
        @Override
        public void afterDisk(FormattedDisk disk) {
            System.out.printf("%s format; %,d bytes free; %,d bytes used.\n",
                      disk.getFormat(),
                      disk.getFreeSpace(),
                      disk.getUsedSpace());
        }
        
        private List<String> createFormatSpec(List<FileColumnHeader> fileColumnHeaders) {
            List<String> fmtSpec = new ArrayList<>();
            for (FileColumnHeader h : fileColumnHeaders) {
                String spec = String.format("%%%s%ds ", h.isRightAlign() ? "" : "-", 
                        h.getMaximumWidth());
                fmtSpec.add(spec);
            }
            return fmtSpec;
        }
    }
    
    public static class TypeOfFileSelection {
        public TypeOfFile typeOfFile() {
            if (filesOnly) {
                return TypeOfFile.FILE;
            }
            if (directoriesOnly) {
                return TypeOfFile.DIRECTORY;
            }
            return TypeOfFile.BOTH;
        }
        
        @Option(names = "--file", description = "Only include files.")
        private boolean filesOnly;
        
        @Option(names = "--directory", description = "Only include directories.")
        private boolean directoriesOnly;
    }
}
