/*
 *  @author Ranides Atterwim <ranides@gmail.com>
 *  @copyright Ranides Atterwim
 *  @license WTFPL
 *  @url http://ranides.net/projects/assira
 */

package net.ranides.assira.io;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import net.ranides.assira.collection.list.JoinList;
import net.ranides.assira.collection.list.VirtualList;
import net.ranides.assira.text.StringUtils;
import net.ranides.assira.text.Strings;

/**
 * Klasa pomocnicza do przekształcania i parsowania ścieżek w systemie plików.
 * @author ranides
 */
public final class PathHelper {
    
    private PathHelper() { }

    /**
     * Lista reprezentująca ścieżkę do pliku rozbitą na fragmenty (gdzie każdy
     * fragment to jeden poziom w systemie plików). Kolejność elementów w liście
     * odpowiada kolejności elementów w "standardowej" ścieżce - pierwszy element
     * to root, następny to podkatalog... i tak aż nazwy wskazywanego pliku,
     * która znajduje się na ostatniej pozycji.
     *
     * <p>
     * Przykładowo ścieżka {@code "/a/b/c/d.txt"} jest reprezentowana jako
     * {@code [ "a", "b", "c", "d.txt" ]}
     * </p>
     */
    public static final class FilePath extends VirtualList<String> {
        private final String[] components;

        FilePath(String[] components) { //NOPMD - private ctor of immutable class
            this.components = components;
        }
        
        FilePath(List<String> components) {
            this.components = components.toArray(new String[components.size()]);
        }
        
        FilePath(String path) {
            String text = path.replace('/', File.separatorChar).replace('\\', File.separatorChar);
            this.components = text.split(Pattern.quote(File.separator));
        }
        
        FilePath(File path) {
            this.components = path.getPath().split(Pattern.quote(File.separator));
        }
        
        static FilePath valueOf(Object value) {
            if(value instanceof FilePath) {
                return (FilePath)value;
            }
            if(value instanceof String) {
                return new FilePath((String)value);
            }
            if(value instanceof File) {
                return new FilePath((File)value);
            }
            throw new IllegalArgumentException("Unsupported value: " + value);
        }

        @Override
        public String get(int index) {
            return components[index];
        }

        @Override
        public int size() {
            return components.length;
        }

        /**
         * Konwertuje ścieżkę na standardowy obiekt {@link File}
         * @return
         */
        public File toFile() {
            return new File(StringUtils.join(components, File.separator));
        }

        @Override
        public String toString() {
            return StringUtils.join(components, File.separator);
        }

    }

    /**
     * Wyciąga z podanej ścieżki nazwę dysku (litera+dwukropek). Metoda tylko dla systemów z
     * rodziny Windows. Na systemach unix-like zwraca pusty String.
     * @param file
     * @return
     */
    public static String getDrive(File file) {
        String path = file.getPath();
        int p = path.indexOf(File.separator);
        if (p > 0) {
            return path.substring(0, p);
        }
        return "";
    }

    /**
     * Rozbija podaną ścieżkę na fragmenty i zwraca wynik w postaci obiektu
     * {@link FilePath}.
     * @param file
     * @return
     * @throws PathConvertException jeśli nie udało się pobrać kanonicznej ścieżki do pliku
     */
    public static FilePath explode(File file) throws PathConvertException {
        try {
            LinkedList<String> list = new LinkedList<>();
            for (File i = file.getCanonicalFile(); null != i; i = i.getParentFile()) {
                if (i.getName().equals("")) {
                    list.addFirst(getDrive(file));
                } else {
                    list.addFirst(i.getName());
                }
            }
            return new FilePath(list);
        } catch (IOException cause) {
            throw new PathConvertException("can't explode path", cause);
        } catch (SecurityException cause) {
            throw new PathConvertException("can't explode path", cause);
        }
    }

    /**
     * Rozbija podaną ścieżkę na fragmenty i zwraca wynik w postaci obiektu
     * {@link FilePath}.
     * @param path
     * @return
     * @throws PathConvertException jeśli nie udało się pobrać kanonicznej ścieżki do pliku
     */
    public static List<String> explode(String path) throws PathConvertException {
        return explode(asFile(path));
    }

    /**
     * Zwraca względną ścieżkę do podanego pliku względem katalogu domowego.
     * @param home ścieżka do katalogu domowego
     * @param file ścieżka do pliku
     * @return
     * @throws PathConvertException jeśli pliki znajdują się na różnych dyskach
     */
    public static String asRelative(FilePath home, FilePath file) throws PathConvertException {
        List<String> output = new ArrayList<>(file.size() + home.size() - 1);
        int common = 0;
        int buffer = 0;
        int hfsize = Math.min(home.size(), file.size());
        // first eliminate common root
        while( common<hfsize && home.get(common).equals(file.get(common)) ) {
            common++;
        }
        if (0 == common) {
            throw new PathConvertException("no common root");
        }

        // for each remaining level in the home path, add a ..
        for(int i=home.size()-common; i>0; i--) {
            output.add("..");
            buffer+= 3;
        }
        // for each level in the file path, add the path
        for(int i=common, n=file.size(); i<n; i++) {
            output.add(file.get(i));
            buffer+= file.get(i).length()+1;
        }
        StringBuilder path = new StringBuilder(buffer);
        path.append(output.get(0));
        for(int i=1,n=output.size(); i<n; i++) {
            path.append(File.separator).append(output.get(i));
        }
        return path.toString();
    }

    /**
     * Zwraca względną ścieżkę do podanego pliku względem katalogu domowego.
     * @param home ścieżka do katalogu domowego
     * @param file ścieżka do pliku
     * @return
     * @throws PathConvertException jeśli pliki znajdują się na różnych dyskach, lub ścieżka
     * {@code home} nie wskazuje na katalog
     */
    public static String asRelative(File home, File file) throws PathConvertException {
        if( home.exists() && !home.isDirectory() ) {
            throw new PathConvertException("can't make path relative, 'home' is not a directory.");
        }
        return asRelative(explode(home), explode(file));
    }

    /**
     * Zwraca względną ścieżkę do podanego pliku względem katalogu domowego.
     * @param home ścieżka do katalogu domowego
     * @param file ścieżka do pliku
     * @return
     * @throws PathConvertException jeśli pliki znajdują się na różnych dyskach, lub ścieżka
     * {@code home} nie wskazuje na katalog
     */
    public static String asRelative(String home, String file) throws PathConvertException {
        return asRelative(asFile(home), asFile(file));
    }

    /**
     * Zwraca względną ścieżkę do podanego pliku względem katalogu bieżącego aplikacji.
     * @param file
     * @return
     * @throws PathConvertException jeśli plik znajduje się na innym dysku, niż katalog bieżący
     */
    public static String asRelative(String file) throws PathConvertException {
        return asRelative(new File("."), asFile(file));
    }

    /**
     * Zwraca względną ścieżkę do podanego pliku względem katalogu bieżącego aplikacji.
     * @param file
     * @return
     * @throws PathConvertException jeśli plik znajduje się na innym dysku, niż katalog bieżący
     */
    public static String asRelative(File file) throws PathConvertException {
        return asRelative(new File("."), file);
    }

    /**
     * Zamienia podaną względną ścieżkę na absolutną. Strategia konwersji jest
     * zależna od systemu operacyjnego oraz wersji JDK. Zobacz opis funkcji
     * {@link File#getAbsolutePath}.
     * @param file
     * @return
     */
    public static String asAbsolute(String file) {
        return asFile(file).getAbsolutePath();
    }

    /**
     * Zamienia podaną względną ścieżkę na absolutną. Strategia konwersji jest
     * zależna od systemu operacyjnego oraz wersji JDK. Aby poznać szczegóły
     * zobacz opis funkcji {@link File#getAbsolutePath}.
     * @param file
     * @return
     */
    public static String asAbsolute(File file) {
        return file.getAbsolutePath();
    }

    /**
     * Normalizuje podaną ścieżkę, eliminując wszystkie zbędne komponenty
     * (np usuwając {@code "a/./b"} albo redukując {@code "a/../b"}.
     * Aby poznać szczegóły zobacz opis funkcji {@link java.net.URI#normalize()}.
     * @param file
     * @return
     */
    public static File asNormalized(File file) {
        if (file.getAbsolutePath().equals("/..")) {
            return new File("/");
        } else {
            return new File(file.toURI().normalize());
        }
    }

    /**
     * Normalizuje podaną ścieżkę, eliminując wszystkie zbędne komponenty
     * (np usuwając {@code "a/./b"} albo redukując {@code "a/../b"}.
     * Aby poznać szczegóły zobacz opis funkcji {@link java.net.URI#normalize()}.
     * @param path
     * @return
     */
    public static String asNormalized(String path) {
        return asNormalized(asFile(path)).getAbsolutePath();
    }

    /**
     * Zwraca rozszerzenie pliku, na który wskazuje ścieżka.
     * @param path
     * @return
     */
    public static String getExtension(String path) {
        int index = path.lastIndexOf(".");
        return (-1==index) ? "" : path.substring(index);
    }

    /**
     * Sprawdza, czy ścieżka wskazuje na plik z podanym rozszerzeniem.
     * @param path
     * @param extension
     * @return
     */
    public static boolean hasExtension(String path, String extension) {
        return path.endsWith(extension);
    }

    /**
     * Zwraca nową ścieżkę zmieniając rozszerzenie pliku, na który wskazywała
     * ścieżka podana jako argument. Funkcja <b>nie</b> wykonuje operacji na
     * systemie plików - jak wszystkie funkcje z klasy {@code PathHelper}, metoda
     * pracuje tylko i wyłącznie na nazwach.
     * @param path
     * @param extension
     * @return
     */
    public static String changeExtension(String path, String extension) {
        int index = path.lastIndexOf(".");
        if(-1==index) {
            return path + extension;
        } else {
            return path.substring(0,index) + extension;
        }
    }

    /**
     * Usuwa z podanej pełnej <b>nazwy</b> pliku rozszerzenie, zwracając nazwę
     * przyjazną dla użytkownika.
     * @param name
     * @return
     */
    public static String getSimpleName(String name) {
        int index = name.lastIndexOf(".");
        return index<0 ? name : name.substring(0, index);
    }

    /**
     * Zwraca ścieżkę wskazującą na katalog zawierający podany plik/katalog.
     * W razie potrzeby funkcja konwertuje ścieżkę do lokalizacji absolutnej
     * (np jeśli zostanie wywołana na rzecz ścieżki "."). Jeśli konwersja nie
     * jest konieczna, podana ścieżka względna nadal zostaje zwrócona w postaci
     * względnej (np {@code "./local/name"} zamieni się na {@code "./local"})
     * @param file
     * @return
     * @throws PathConvertException
     */
    public static File getParent(File file) throws PathConvertException {
        String parent = file.getParent();
        if(Strings.isEmpty(parent)) {
            String path = file.getAbsolutePath();
            String name = Strings.or(file.getName(),"");
            parent = path.substring(0, path.length() - name.length());
            if( Strings.isEmpty(parent) ) {
                throw new PathConvertException("file has no parent");
            }
        }
        return new File(parent);
    }

    public static File asFile(String path) {
        return new File(path.replace('/', File.separatorChar).replace('\\', File.separatorChar));
    }

    public static int compare(String path1, String path2) {
        return asFile(path1).compareTo(asFile(path2));
    }
    
    public static String join(FilePath... paths) {
        return new FilePath(new JoinList<>(paths)).toString();
    }
    
    public static String join(File... paths) throws PathConvertException {
        FilePath[] out = new FilePath[paths.length];
        for(int i=0; i<paths.length; i++) {
            out[i] = new FilePath(paths[i]);
        }
        return join(out);
    }
    
    public static String join(String... paths) throws PathConvertException {
        FilePath[] out = new FilePath[paths.length];
        for(int i=0; i<paths.length; i++) {
            out[i] = new FilePath(paths[i]);
        }
        return join(out);
    }
    
    public static String join(Object... paths) {
        FilePath[] out = new FilePath[paths.length];
        for(int i=0; i<paths.length; i++) {
            out[i] = FilePath.valueOf(paths[i]);
        }
        return join(out);
    }

}
