package com.athaydes.logfx.config;

import com.athaydes.logfx.data.LogLineColors;
import com.athaydes.logfx.text.HighlightExpression;
import javafx.geometry.Orientation;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;

final class ConfigParser {

    enum ConfigVersion {
        V1, V2;

        public boolean isAfterV1() {
            return this == V2;
        }
    }

    private static final Logger log = LoggerFactory.getLogger( ConfigParser.class );

    private final ConfigProperties properties;
    private ConfigVersion version = ConfigVersion.V1;

    ConfigParser( ConfigProperties properties ) {
        this.properties = properties;
    }

    void parseConfigFile( String currentLine, Iterator<String> lines ) {
        String line;
        if ( currentLine != null ) {
            line = currentLine;
        } else if ( lines.hasNext() ) {
            line = lines.next();
        } else {
            return; // no lines left
        }

        switch ( line.trim() ) {
            case "version:":
                parseVersion( lines );
            case "standard-log-colors:":
                parseStandardLogColors( lines );
            case "expressions:":
                parseExpressions( lines );
            case "filters:":
                parseFilters( lines );
            case "files:":
                parseFiles( lines );
            case "gui:":
                parseGuiSection( lines );
        }
    }

    private void parseVersion( Iterator<String> lines ) {
        if ( lines.hasNext() ) {
            String line = lines.next();
            if ( line.startsWith( " " ) ) {
                try {
                    version = ConfigVersion.valueOf( line.trim() );
                } catch ( IllegalArgumentException e ) {
                    logInvalidProperty( "version", "version", line.trim(), "Unknown version" );
                }
            } else {
                parseConfigFile( line, lines );
            }
        }
    }

    private void parseStandardLogColors( Iterator<String> lines ) {
        while ( lines.hasNext() ) {
            String line = lines.next();
            if ( line.startsWith( " " ) ) {
                String[] components = line.trim().split( " " );
                if ( components.length == 2 ) {
                    try {
                        Color bkg = Color.valueOf( components[ 0 ] );
                        Color fill = Color.valueOf( components[ 1 ] );
                        properties.standardLogColors.set( new LogLineColors( bkg, fill ) );
                    } catch ( IllegalArgumentException e ) {
                        logInvalidProperty( "standard-log-colors", "color", line, e.toString() );
                    }
                } else {
                    logInvalidProperty( "standard-log-colors", "number of colors", line, null );
                }
            } else if ( !line.trim().isEmpty() ) {
                parseConfigFile( line, lines );
                break;
            }
        }
    }

    private void parseExpressions( Iterator<String> lines ) {
        while ( lines.hasNext() ) {
            String line = lines.next();
            if ( line.startsWith( " " ) ) {
                try {
                    properties.observableExpressions.add( parseHighlightExpression( line.trim(), version ) );
                } catch ( IllegalArgumentException e ) {
                    logInvalidProperty( "expressions", "highlight", line, e.toString() );
                }
            } else if ( !line.trim().isEmpty() ) {
                parseConfigFile( line, lines );
                break;
            }
        }
    }

    private void parseFilters( Iterator<String> lines ) {
        if ( lines.hasNext() ) {
            String line = lines.next();
            if ( line.startsWith( " " ) ) {
                switch ( line.trim() ) {
                    case "enable":
                        properties.enableFilters.set( true );
                        break;
                    case "disable":
                        properties.enableFilters.set( false );
                        break;
                    default:
                        logInvalidProperty( "filters", "filters", line, "value must be 'enable' or 'disable'" );
                }
            } else if ( !line.trim().isEmpty() ) {
                parseConfigFile( line, lines );
            }
        }
    }

    private void parseFiles( Iterator<String> lines ) {
        while ( lines.hasNext() ) {
            String line = lines.next();
            if ( line.startsWith( " " ) ) {
                properties.observableFiles.add( new File( line.trim() ) );
            } else if ( !line.trim().isEmpty() ) {
                parseConfigFile( line, lines );
                break;
            }
        }
    }

    private void parseGuiSection( Iterator<String> lines ) {
        while ( lines.hasNext() ) {
            String line = lines.next();
            if ( line.startsWith( " " ) ) {
                String[] parts = line.trim().split( "\\s+" );
                if ( parts.length == 0 ) {
                    logInvalidProperty( "gui", "?", "empty line", null );
                } else switch ( parts[ 0 ] ) {
                    case "orientation":
                        if ( parts.length != 2 ) {
                            logInvalidProperty( "gui", "orientation", line,
                                    "Expected 2 parts, got " + parts.length );
                        } else try {
                            properties.panesOrientation.set( Orientation.valueOf( parts[ 1 ] ) );
                        } catch ( IllegalArgumentException e ) {
                            logInvalidProperty( "gui", "orientation", parts[ 1 ],
                                    "Invalid value for Orientation: " + e );
                        }
                        break;
                    case "pane-dividers":
                        if ( parts.length != 2 ) {
                            logInvalidProperty( "gui", "pane-dividers", line,
                                    "Expected 2 parts, got " + parts.length );
                        } else try {
                            String[] separators = parts[ 1 ].split( "," );
                            properties.paneDividerPositions.addAll( toDoubles( separators ) );
                        } catch ( IllegalArgumentException e ) {
                            logInvalidProperty( "gui", "pane-dividers", parts[ 1 ],
                                    e.toString() );
                        }
                        break;
                    case "font":
                        if ( parts.length < 3 ) {
                            logInvalidProperty( "gui", "font", line,
                                    "Expected 3 or more parts, got " + parts.length );
                        } else {
                            double size = 12.0;
                            try {
                                size = Double.parseDouble( parts[ 1 ] );
                            } catch ( NumberFormatException e ) {
                                logInvalidProperty( "gui", "font", parts[ 1 ],
                                        "Font size is invalid: " + parts[ 1 ] );
                            }

                            String[] fontNameParts = Arrays.copyOfRange( parts, 2, parts.length );
                            String fontName = String.join( " ", fontNameParts );
                            try {
                                properties.font.setValue( Font.font( fontName, size ) );
                            } catch ( IllegalArgumentException e ) {
                                logInvalidProperty( "gui", "font", parts[ 0 ],
                                        "Invalid font name: " + fontName );
                            }
                        }
                        break;
                }
            } else if ( !line.trim().isEmpty() ) {
                parseConfigFile( line, lines );
                break;
            }
        }
    }

    static HighlightExpression parseHighlightExpression( String line, ConfigVersion version )
            throws IllegalArgumentException {
        if ( line.isEmpty() ) {
            throw highlightParseError( "empty highlight expression", line );
        }
        if ( line.startsWith( " " ) || line.endsWith( " " ) ) {
            throw highlightParseError( "highlight expression contains invalid spaces at start/end", line );
        }

        int firstSpaceIndex = line.indexOf( ' ' );
        if ( firstSpaceIndex <= 0 ) {
            throw highlightParseError( "fill color and regular expression not specified", line );
        }
        String bkgColorString = line.substring( 0, firstSpaceIndex );

        Color bkgColor;
        try {
            bkgColor = Color.valueOf( bkgColorString );
        } catch ( IllegalArgumentException e ) {
            throw highlightParseError( "invalid background color: '" + bkgColorString + "'", line );
        }

        if ( line.length() > firstSpaceIndex + 1 ) {
            line = line.substring( firstSpaceIndex + 1 ).trim();
        } else {
            throw highlightParseError( "fill color and regular expression not specified", line );
        }

        int secondSpaceIndex = line.indexOf( ' ' );
        if ( secondSpaceIndex <= 0 ) {
            throw highlightParseError( "regular expression not specified", line );
        }

        String fillColorString = line.substring( 0, secondSpaceIndex );
        Color fillColor;
        try {
            fillColor = Color.valueOf( fillColorString );
        } catch ( IllegalArgumentException e ) {
            throw highlightParseError( "invalid fill color: '" + fillColorString + "'", line );
        }

        boolean isFiltered = false;

        if ( version.isAfterV1() ) {
            if ( line.length() > secondSpaceIndex + 1 ) {
                line = line.substring( secondSpaceIndex + 1 ).trim();
            } else {
                throw highlightParseError( "filter and regular expression not specified", line );
            }

            int thirdSpaceIndex = line.indexOf( ' ' );
            if ( thirdSpaceIndex <= 0 ) {
                throw highlightParseError( "regular expression not specified", line );
            }
            String filteredString = line.substring( 0, thirdSpaceIndex );
            switch ( filteredString.toLowerCase() ) {
                case "true":
                    isFiltered = true;
                    break;
                case "false":
                    isFiltered = false;
                    break;
                default:
                    throw highlightParseError( "invalid value for filtered property", line );
            }

            if ( line.length() > thirdSpaceIndex + 1 ) {
                line = line.substring( thirdSpaceIndex + 1 ).trim();
            } else {
                line = "";
            }
        } else {
            if ( line.length() > secondSpaceIndex + 1 ) {
                line = line.substring( secondSpaceIndex + 1 ).trim();
            } else {
                line = "";
            }
        }

        if ( line.isEmpty() ) {
            throw highlightParseError( "regular expression not specified", line );
        }

        String expression = line;
        try {
            return new HighlightExpression( expression, bkgColor, fillColor, isFiltered );
        } catch ( PatternSyntaxException e ) {
            throw highlightParseError( "cannot parse regular expression '" + expression + "'", line );
        }
    }

    private static IllegalArgumentException highlightParseError( String error, String line ) {
        return new IllegalArgumentException( "Invalid highlight expression [" + error + "]: " + line );
    }

    private static List<Double> toDoubles( String[] numbers ) {
        return Stream.of( numbers )
                .map( String::trim )
                .map( Double::parseDouble )
                .collect( toList() );
    }

    private static void logInvalidProperty( String section, String name,
                                            String invalidValue, String error ) {
        log.warn( "Invalid value for {} in section {}: '{}'",
                name, section, invalidValue );
        if ( error != null ) {
            log.warn( "Error: {}", error );
        }
    }
}