import * as React from 'react';
import { isNullOrUndefined } from 'util';
import { Box, TextField, Button, Typography, Table, TableBody, TableRow, TableCell, CircularProgress, Grid } from '@material-ui/core';
import Analyzer from '../Analyzer';
import VigenereCipher from '../../ciphers/cipherProtocols/VigenereCipher';
import { passwordLoad, myAlert, writerToOutput, chosenAlphabet, inputLoad, chosenDatabase, regexp, HashMap, bigramDatabase, sumOfBiDatabase, qgramDatabase, sumOfQDatabase, scroller } from '../../basicLayout';


export interface IVigenereAnalyzerProps {
    idInput: string,
    idOutput: string,
    idSwitch: string
}

export interface IVigenereAnalyzerState {
    vigenereAnalyerOutput: string
}

export default class VigenereAnalyzer extends React.Component<IVigenereAnalyzerProps, IVigenereAnalyzerState> {
    private vigenere: any = new VigenereCipher(this.props);
    private analyzer: any = new Analyzer(this.props);
    private passwordLength: number[] = [];
    private expectedLangValue: number = 1.73;

    private inputLength: number = 0;
    private computedPasswordLength: number = 0;
    private computedPassword: string = '---';
    private passwordMap: any = new HashMap();

    private noEnglish: boolean = false;

    constructor(props: IVigenereAnalyzerProps) {
        super(props);

        this.state = {
            vigenereAnalyerOutput: ''
        }

        this.lengthCorrelation = this.lengthCorrelation.bind(this);
        this.passwordLengthComputation = this.passwordLengthComputation.bind(this);
        this.passwordConstruction = this.passwordConstruction.bind(this);
        this.passwordLengthLoader = this.passwordLengthLoader.bind(this);
        this.overwiewTable = this.overwiewTable.bind(this);
        this.breakPasswordButton = this.breakPasswordButton.bind(this);
    }

    private passwordAlertBuilder(showAlert: number[]) {
        var result: string[] = [];
        if (showAlert[0] === 1)
            result.push('too many arguments');
        if (showAlert[1] === 1)
            result.push('other characters than number');
        if (showAlert[2] === 1)
            result.push('min 2, max 99');

        return result;
    }

    private passwordLengthLoader(tmpString: string): string[] {
        var alerts: string[] = [];
        var tmpArray: string[] = tmpString.split('-');
        var showAlert: number[] = [0, 0, 0];
        this.passwordLength = [];

        tmpArray.forEach((element: any) => {
            if (this.passwordLength.length === 2) {
                showAlert[0] = 1;      //'Hold prediscribed form (too many arguments)!'
                return;
            }

            var eSplit = element.split('');
            var tmp = '';

            eSplit.forEach((num: any) => {
                if (num.match(/[0-9]/))
                    tmp += num;
                else
                    showAlert[1] = 1   //'Hold prediscribed form (characters in number)!'
            });

            if ((Number(tmp) <= 99) && (Number(tmp) >= 2))
                this.passwordLength.push(Number(tmp));
            else
                showAlert[2] = 1       //'Hold prediscribed form (min 2, max 99)!'
        });

        if (this.passwordLength[0] > this.passwordLength[1])
            this.passwordLength.sort();

        if (showAlert !== [0, 0, 0])
            alerts = this.passwordAlertBuilder(showAlert);

        return alerts;
    }

    //  PROBABLE PASSWORD-SIZE COMPUTATION
    private lengthCorrelation(blocks: string[], length: number): number {
        var columnICSum: number = 0;

        for (let i = 0; i < length; i++) {
            let column: string = '';

            blocks.forEach(e => {
                if (e[i] !== undefined)
                    column += e[i];
            })

            columnICSum += this.analyzer.indexOfCoincidence(column.toLowerCase());
        }

        return columnICSum / length;
    }

    private passwordLengthComputation(input: string): number {
        var minLength: number = Number.MAX_VALUE;
        var currentPsswdLength: number = 1;
        var probableLength: number = 0;

        while (currentPsswdLength < chosenAlphabet.length) {
            var tmpCorrelationResult: number = this.lengthCorrelation(this.splitter(input, currentPsswdLength), currentPsswdLength);
            var tmpDifference: number = Math.abs(this.expectedLangValue - tmpCorrelationResult);

            if (minLength > tmpDifference) {
                minLength = tmpDifference;
                probableLength = currentPsswdLength;
            }
            currentPsswdLength++;
        }

        return probableLength;
    }

    //  PROBABLE PASSWORD COMPUTATION
    private splitter(text: string, size: number): any[] {
        var input: string = '';
        text.split('').forEach(e => {
            if (e.match(regexp))
                input += e;
        });

        var result: any[] = [];
        var pointer: number = 0;
        var block: number = 0;

        while (pointer < input.length) {
            result[block] = '';
            for (var i = 0; i < size; i++) {
                if(pointer > input.length-1)
                    break;
                result[block] += input.charAt(pointer++);
            }

            block++;
        }

        return result;
    }

    private bigramKeysGenerator(): string[] {
        let pointer: number = 0
        let childKeys: string[] = [];
        for (let firstLetter: number = 0; firstLetter < chosenAlphabet.length; firstLetter++)
            for (let secondLetter: number = 0; secondLetter < chosenAlphabet.length; secondLetter++)
                childKeys[pointer++] = chosenAlphabet[firstLetter] + chosenAlphabet[secondLetter];

        return childKeys;
    }

    private fitness(bigram: string): number {
        if (bigramDatabase.has(bigram))
            return Math.log10(bigramDatabase.get(bigram)/sumOfBiDatabase);
        else
            return Math.log10(0.01/sumOfBiDatabase);
    }

    private Qfitness(qgram: string): number {
        if (qgramDatabase.has(qgram))
            return Math.log10(qgramDatabase.get(qgram)/sumOfQDatabase);
        else
            return Math.log10(0.01/sumOfQDatabase);
    }

    private bestBigramPasswordSearcher(bigramMap: any): any {
        let generatedPasswords: string[] = this.bigramKeysGenerator();
        let bestPassword: string = '';
        let maxFitness: number = -Number.MAX_VALUE;

        for (let password of generatedPasswords) {
            let tmpFitness: number = 0;

            for (let bigram of bigramMap.keys()) {
                let tmpBigram: string = this.vigenere.VigenereCipher(bigram.split(''), chosenAlphabet, password, true);

                tmpFitness += this.fitness(tmpBigram) * bigramMap.get(bigram);
            }

            if (maxFitness < tmpFitness) {
                maxFitness = tmpFitness;
                bestPassword = password;
            }
        }

        return { bestPassword, maxFitness };
    }

    private passwordBuilder(length: number): any {
        var input: string = inputLoad();
        var probablePassword: string = '';
        var probablePasswordFitness: number = 0;

        var lastFitness: number = 0;

        var rows: string[] = this.splitter(input.toUpperCase(), length);
        if(rows[rows.length-1].length !== length)
            rows.pop();
        
        var firstLetter: string[] = [];
        rows.forEach((row: any) => {
            firstLetter.push(row.charAt(0));
        });

        for (var rowsColumnIndex = 0; rowsColumnIndex < length; rowsColumnIndex++) {
            let bigramColumn: any = new HashMap();
            rows.forEach((row: string, index: number) => {
                //  forming bigram field from columns 0,1; 1,2 ... n,0
                let tmpBigram: string = '';
                if (row.length > 1) {
                    tmpBigram = row.substr(0, 2);
                    rows[index] = row.slice(1);
                }
                else if(row.length === 1) {
                    tmpBigram = (row + firstLetter[index]);
                    rows[index] = row.slice(1);
                }

                //  inserting bigram to map + if bigram exists -> rise count
                if(bigramColumn.has(tmpBigram)) {
                    var tmpValue: number = bigramColumn.get(tmpBigram) + 1;
                    bigramColumn.set(tmpBigram, tmpValue);
                }
                else
                    bigramColumn.set(tmpBigram, 1);
            });

            //  inserting formed column bigram field to hash map
            var tmpResult: any = this.bestBigramPasswordSearcher(bigramColumn);
    
            if(probablePassword.length !== length || rowsColumnIndex < length) {
                if(lastFitness === 0) {
                    //  inicialization
                    probablePassword += tmpResult.bestPassword;
            
                    lastFitness = tmpResult.maxFitness;
                }
                else if(lastFitness > tmpResult.maxFitness) {
                    //  take 2nd letter form bestPassword (because lastFitness is better)
                    probablePassword += tmpResult.bestPassword.slice(1);
                    lastFitness = tmpResult.maxFitness;
                }
                else {
                    //  erase last letter form probablePassword and add whole bestPassword
                    probablePassword = probablePassword.slice(0, probablePassword.length-1);
                    probablePassword += tmpResult.bestPassword;
            
                    lastFitness = tmpResult.maxFitness;
                }
            }
            else {  //  treatment of the final length of the probablePassword
                if(probablePassword.length > length)
                    //  erase last letter if length bigger
                    probablePassword = probablePassword.slice(0, probablePassword.length-1);
                else if(lastFitness < tmpResult.maxFitness) {
                    //  erase last letter form probablePassword and add first letter form bestPassowrd if length correct but lastFitness is worse
                    probablePassword = probablePassword.slice(0, probablePassword.length-1);
                    probablePassword += tmpResult.bestPassword.slice(0,1);
                }
            }
        }

        if(probablePassword.length !== length)
            probablePassword = probablePassword = probablePassword.slice(0, probablePassword.length-1);

        //  COUTING PROBABLE FITNESS OF THE WHOLE DECRYPTED TEXT WITH PROBABLE PASSWORD
        var decipheredText: string[] = this.splitter(this.vigenere.VigenereCipher(input.split(''), chosenAlphabet, probablePassword, true), 4);
        for(var qgram of decipheredText)
            probablePasswordFitness += this.Qfitness(qgram.toUpperCase());

        return { probablePassword, probablePasswordFitness };
    }

    private findMinFitnessInMap(map: any): string {
        var bestPassword: string = '';
        var bestFitness: number = -Number.MAX_VALUE;
        var bestKey: number = 0;

        console.log('\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n~ ALL RESULTS FROM PASSWORD COMPUTATION ~\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
        map.forEach((value: any, key: number) => {
            console.log('[' + key + ']:\nPassword: ' + value.probablePassword + '\nFitness: ' + value.probablePasswordFitness + '\n------');

            if (bestFitness < value.probablePasswordFitness) {
                bestPassword = value.probablePassword;
                bestFitness = value.probablePasswordFitness;
                bestKey = key;
            }
        });

        this.computedPassword = bestPassword + ' (key length: ' + bestKey + ')';
        (document.getElementById('pressf12statement') as HTMLInputElement).hidden = false;

        return bestPassword;
    }

    private passwordConstruction(): string {
        var password: string = '';
        this.passwordMap = new HashMap();
        var num: number = 2;
        var until: number = chosenAlphabet.length;

        if (this.passwordLength.length === 1) {
            password = this.passwordBuilder(this.passwordLength[0]).probablePassword;
            this.computedPassword = password + ' (key length: ' + this.passwordLength[0] + ')';
            return password;
        }
        else if (this.passwordLength.length === 2) {
            num = this.passwordLength[0];
            until = this.passwordLength[1]+1;
        }

        for (num; num < until; num++) {
            var tmpResult: any = this.passwordBuilder(num);
            this.passwordMap.set(num, tmpResult);
        }
        
        return this.findMinFitnessInMap(this.passwordMap);
    }

    private isThisVigenere() {
        var message: string = this.vigenere.VigenereCipher(inputLoad().split(''), chosenAlphabet, passwordLoad("myPasswordVigenereField").toLowerCase().split(''), true);
        return message;
    }

    
    /*////////////////////////////////////////////////////////////////////////////////////////////////////////*/
    /*////////////////////////////////////////////////////////////////////////////////////////////////////////*/
    /*////////////////////////////////////////////////////////////////////////////////////////////////////////*/

    //  LAYOUT

    private overwiewTable() {
        var input: string = inputLoad();
        this.inputLength = input.length;
        
        this.computedPasswordLength = this.passwordLengthComputation(input);

        return (
            <div>
                <Box marginTop={3} textAlign='left' boxShadow={2}>
                    <Table id='table' className="table" padding="default">
                        <TableBody>
                            <TableRow>
                                <TableCell variant='head'>Input length:</TableCell><TableCell><b>{this.inputLength}</b></TableCell>
                            </TableRow>
                            <TableRow>
                                <TableCell variant='head'>Computed password length:</TableCell><TableCell>{this.computedPasswordLength}</TableCell>
                            </TableRow>
                            <TableRow>
                                <TableCell variant='head'>Best computed password:</TableCell><TableCell>{this.computedPassword}</TableCell>
                            </TableRow>
                        </TableBody>
                    </Table>
                </Box>
            </div>
        );
    }

    private languageDetermination() {
        if (chosenDatabase === 'eng') {
            if(document.getElementById('engAlert'))
                (document.getElementById('engAlert') as HTMLInputElement).innerText = '';
            this.expectedLangValue = 1.73;
            this.noEnglish = false;
        }
        else if (chosenDatabase === 'cze') {
            if(document.getElementById('engAlert'))
                (document.getElementById('engAlert')as HTMLInputElement).innerText = 'This feature works properly ONLY with ENGLISH - switch to it above!';
            this.noEnglish = true;
        }
        else
            myAlert('Error within passwordSizeGuesser!', 'error', chosenDatabase);
    }

    private breakPasswordButton() {
        const [loading, setLoading] = React.useState(false);
        const handlePasswordClick = () => {
            var tmpString: string = (document.getElementById('passwordLengthRange') as HTMLInputElement)?.value;
            if (tmpString === '' || isNullOrUndefined(tmpString)) {
                if(this.inputLength < 500)
                    tmpString = String(myAlert('Please define range or length of a password!', 'enter', ['2-25']));
                else
                    tmpString = String(myAlert('Please define range or length of a password! Consider also small range around computed password length because of the length of the input (otherwise it could take REALLY long to process).', 'enter', [this.computedPasswordLength]));
                
                if(tmpString === 'undefined')
                    return;
                
                (document.getElementById('passwordLengthRange') as HTMLInputElement).value = tmpString;
            }

            var showAlert: string[] = this.passwordLengthLoader(tmpString);
            if (showAlert.length >= 1)
                myAlert('Hold prediscribed form (' + showAlert.toString() + ')!', 'info');
            
            setLoading(true);
            if (!loading) {
                global.setTimeout(() => {
                    var password: string = this.passwordConstruction();
                    (document.getElementById('myPasswordVigenereField') as HTMLInputElement).value = password;

                    this.forceUpdate();
                    setLoading(false);
                }, 500);
            }
        };

        this.languageDetermination();

        return(
            <Grid container className='columnGrid'>
                <Button
                    id='guesser'
                    size="small"
                    variant="text"
                    autoFocus
                    disabled={this.noEnglish}
                    onClick={handlePasswordClick}
                >
                    <Typography variant='caption' style={{ color: '#808080' }}>Guess password?</Typography>
                </Button>
                
                {loading && 
                    <Box>
                        <Typography variant='caption' style={{ color: '#808080', position: 'fixed', float: 'left', top: 60, left: 10}}>Loading...</Typography>
                        <CircularProgress size={45} thickness={8} style={{ position: 'fixed', float: 'left', top: 10, left: 10 }} />
                    </Box>}
            </Grid>
        );
    }

    public render() {
        const handleVigenereClick = (event: React.MouseEvent<HTMLElement>) => {
            if ((document.getElementById("myPasswordVigenereField") as HTMLInputElement).value === '') {
                var tmp = myAlert('Please fill the password field!', 'enter', ['secret']);
                if (tmp != null) {
                    (document.getElementById("myPasswordVigenereField") as HTMLInputElement).value = String(tmp);
                }
                else
                    return;
            }

            writerToOutput(this.isThisVigenere());

            scroller();
            
            this.forceUpdate();
        }

        this.languageDetermination();
        
        return (
            <div>
                <Typography id='engAlert' color='error' variant='caption'/>
                <Box marginBottom={2}>
                    <TextField
                        required
                        id="myPasswordVigenereField"
                        label="Password"
                        helperText="Please typedown the chosen password."
                        disabled={this.noEnglish}
                        InputLabelProps={{
                            shrink: true
                        }}
                        className='smallTextField'
                    />
                    <TextField
                        required
                        id="passwordLengthRange"
                        label="Password length"
                        placeholder="e.g. 3-8 or 5"
                        helperText="Password length for computation (min 2, max 99)."
                        disabled={this.noEnglish}
                        InputLabelProps={{
                            shrink: true
                        }}
                        className='smallTextField'
                    />
                </Box>
                <Box my={2}>
                    <Button
                        id='action'
                        size="small"
                        variant="contained"
                        onClick={handleVigenereClick}
                        disabled={this.noEnglish}
                    >Is this Vigenère?</Button>
                </Box>
                <this.breakPasswordButton/>
                <this.overwiewTable />
                <Typography id='pressf12statement' variant='caption' hidden={true}>
                    Isn't what you are looking for? See browser console for all results! (You'll get there by <b>pressing F12</b>.)
                </Typography>
            </div>
        );
    }
}