/**
 * Copyright (C) 2014 SignalFuse, Inc.
 */
package com.github.dockerjava.core;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import org.apache.commons.lang3.StringUtils;

import com.github.dockerjava.core.exception.GoLangFileMatchException;

/**
 * Implementation of golang's file.Match
 *
 * Match returns true if name matches the shell file name pattern. The pattern syntax is:
 *
 * <pre>
 *  pattern:
 *          { term }
 *   term:
 *       '*'         matches any sequence of non-Separator characters
 *      '?'         matches any single non-Separator character
 *       '[' [ '^' ] { character-range } ']'
 *                  character class (must be non-empty)
 *       c           matches character c (c != '*', '?', '\\', '[')
 *       '\\' c      matches character c
 *
 *   character-range:
 *       c           matches character c (c != '\\', '-', ']')
 *       '\\' c      matches character c
 *       lo '-' hi   matches character c for lo <= c <= hi
 *
 *  Match requires pattern to match all of name, not just a substring.
 *  The only possible returned error is ErrBadPattern, when pattern
 *  is malformed.
 *
 * On Windows, escaping is disabled. Instead, '\\' is treated as
 AuthConfigTest *  path separator.
 * </pre>
 *
 * @author tedo
 *
 */
public class GoLangFileMatch {
    private GoLangFileMatch() {
    }

    public static final boolean IS_WINDOWS = File.separatorChar == '\\';

    private static final String PATTERN_CHARS_TO_ESCAPE = "\\.[]{}()*+-?^$|";

    private static final LoadingCache<String, Pattern> PATTERN_CACHE = CacheBuilder.newBuilder()
        .expireAfterAccess(1, TimeUnit.HOURS)
        .maximumSize(10_000)
        .build(CacheLoader.from(GoLangFileMatch::buildPattern));

    public static boolean match(List<String> patterns, File file) {
        return !match(patterns, file.getPath()).isEmpty();
    }

    public static boolean match(String pattern, File file) {
        return match(pattern, file.getPath());
    }

    /**
     * Returns the matching patterns for the given string
     */
    public static List<String> match(List<String> patterns, String name) {
        List<String> matches = new ArrayList<>();
        for (String pattern : patterns) {
            if (match(pattern, name)) {
                matches.add(pattern);
            }
        }
        return matches;
    }

    public static boolean match(String pattern, String name) {
        try {
            return PATTERN_CACHE.get(pattern).matcher(name).matches();
        } catch (ExecutionException | UncheckedExecutionException e) {
            throw new GoLangFileMatchException(e.getCause().getMessage());
        }
    }

    private static Pattern buildPattern(String pattern) {
        StringBuilder patternStringBuilder = new StringBuilder("^");
        while (!pattern.isEmpty()) {
            pattern = appendChunkPattern(patternStringBuilder, pattern);

            if (!pattern.isEmpty()) {
                patternStringBuilder.append(quote(File.separatorChar));
            }
        }
        patternStringBuilder.append("(").append(quote(File.separatorChar)).append(".*").append(")?");
        return Pattern.compile(patternStringBuilder.toString());
    }

    private static String quote(char separatorChar) {
        if (StringUtils.contains(PATTERN_CHARS_TO_ESCAPE, separatorChar)) {
            return "\\" + separatorChar;
        } else {
            return String.valueOf(separatorChar);
        }
    }

    private static String appendChunkPattern(StringBuilder patternStringBuilder, String pattern) {
        if (pattern.equals("**") || pattern.startsWith("**" + File.separator)) {
            patternStringBuilder.append("(")
                    .append("[^").append(quote(File.separatorChar)).append("]*")
                    .append("(")
                    .append(quote(File.separatorChar)).append("[^").append(quote(File.separatorChar)).append("]*")
                    .append(")*").append(")?");
            return pattern.substring(pattern.length() == 2 ? 2 : 3);
        }

        boolean inRange = false;
        int rangeFrom = 0;
        RangeParseState rangeParseState = RangeParseState.CHAR_EXPECTED;
        boolean isEsc = false;
        int i;
        for (i = 0; i < pattern.length(); i++) {
            char c = pattern.charAt(i);
            switch (c) {
                case '/':
                    if (!inRange) {
                        if (!IS_WINDOWS && !isEsc) {
                            // end of chunk
                            return pattern.substring(i + 1);
                        } else {
                            patternStringBuilder.append(quote(c));
                        }
                    } else {
                        rangeParseState = nextStateAfterChar(rangeParseState);
                    }
                    isEsc = false;
                    break;
                case '\\':
                    if (!inRange) {
                        if (!IS_WINDOWS) {
                            if (isEsc) {
                                patternStringBuilder.append(quote(c));
                                isEsc = false;
                            } else {
                                isEsc = true;
                            }
                        } else {
                            // end of chunk
                            return pattern.substring(i + 1);
                        }
                    } else {
                        if (IS_WINDOWS || isEsc) {
                            rangeParseState = nextStateAfterChar(rangeParseState);
                            isEsc = false;
                        } else {
                            isEsc = true;
                        }
                    }
                    break;
                case '[':
                    if (!isEsc) {
                        if (inRange) {
                            throw new GoLangFileMatchException("[ not expected, closing bracket ] not yet reached");
                        }
                        rangeFrom = i;
                        rangeParseState = RangeParseState.CHAR_EXPECTED;
                        inRange = true;
                    } else {
                        if (!inRange) {
                            patternStringBuilder.append(c);
                        } else {
                            rangeParseState = nextStateAfterChar(rangeParseState);
                        }
                    }
                    isEsc = false;
                    break;
                case ']':
                    if (!isEsc) {
                        if (!inRange) {
                            throw new GoLangFileMatchException("] is not expected, [ was not met");
                        }
                        if (rangeParseState == RangeParseState.CHAR_EXPECTED_AFTER_DASH) {
                            throw new GoLangFileMatchException("Character range not finished");
                        }
                        patternStringBuilder.append(pattern.substring(rangeFrom, i + 1));
                        inRange = false;
                    } else {
                        if (!inRange) {
                            patternStringBuilder.append(c);
                        } else {
                            rangeParseState = nextStateAfterChar(rangeParseState);
                        }
                    }
                    isEsc = false;
                    break;
                case '*':
                    if (!inRange) {
                        if (!isEsc) {
                            patternStringBuilder.append("[^").append(quote(File.separatorChar)).append("]*");
                        } else {
                            patternStringBuilder.append(quote(c));
                        }
                    } else {
                        rangeParseState = nextStateAfterChar(rangeParseState);
                    }
                    isEsc = false;
                    break;
                case '?':
                    if (!inRange) {
                        if (!isEsc) {
                            patternStringBuilder.append("[^").append(quote(File.separatorChar)).append("]");
                        } else {
                            patternStringBuilder.append(quote(c));
                        }
                    } else {
                        rangeParseState = nextStateAfterChar(rangeParseState);
                    }
                    isEsc = false;
                    break;
                case '-':
                    if (!inRange) {
                        patternStringBuilder.append(quote(c));
                    } else {
                        if (!isEsc) {
                            if (rangeParseState != RangeParseState.CHAR_OR_DASH_EXPECTED) {
                                throw new GoLangFileMatchException("- character not expected");
                            }
                            rangeParseState = RangeParseState.CHAR_EXPECTED_AFTER_DASH;
                        } else {
                            rangeParseState = nextStateAfterChar(rangeParseState);
                        }
                    }
                    isEsc = false;
                    break;
                default:
                    if (!inRange) {
                        patternStringBuilder.append(quote(c));
                    } else {
                        rangeParseState = nextStateAfterChar(rangeParseState);
                    }
                    isEsc = false;
            }
        }
        if (isEsc) {
            throw new GoLangFileMatchException("Escaped character missing");
        }
        if (inRange) {
            throw new GoLangFileMatchException("Character range not finished");
        }
        return "";
    }

    private static RangeParseState nextStateAfterChar(RangeParseState currentState) {
        if (currentState == RangeParseState.CHAR_EXPECTED_AFTER_DASH) {
            return RangeParseState.CHAR_EXPECTED;
        } else {
            return RangeParseState.CHAR_OR_DASH_EXPECTED;
        }
    }

    private enum RangeParseState {
        CHAR_EXPECTED,
        CHAR_OR_DASH_EXPECTED,
        CHAR_EXPECTED_AFTER_DASH
    }

}
