'use strict';

import { ArgumentNullError, ParserError } from './Errors';
import { Range, Position } from './Positioning';
import { X12Diagnostic, X12DiagnosticLevel } from './X12Diagnostic';
import { X12Interchange } from './X12Interchange';
import { X12FunctionalGroup } from './X12FunctionalGroup';
import { X12Transaction } from './X12Transaction';
import { X12Segment } from './X12Segment';
import { X12Element } from './X12Element';

const DOCUMENT_MIN_LENGTH: number = 113; // ISA = 106, IEA > 7
const SEGMENT_TERMINATOR_POS: number = 105;
const ELEMENT_DELIMITER_POS: number = 3;
const INTERCHANGE_CACHE_SIZE: number = 10;

export class X12Parser {
    constructor(private _strict: boolean) {
        this.diagnostics = new Array<X12Diagnostic>();
    }
    
    diagnostics: X12Diagnostic[];
    
    parseX12(edi: string): X12Interchange {
        if (!edi) {
            throw new ArgumentNullError('edi');
        }
        
        this.diagnostics.splice(0);
        
        if (edi.length < DOCUMENT_MIN_LENGTH) {
            let errorMessage = `X12 Standard: Document is too short. Document must be at least ${DOCUMENT_MIN_LENGTH} characters long to be well-formed X12.`;
            
            if (this._strict) {
                throw new ParserError(errorMessage);
            }
            
            this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, new Range(0, 0, 0, edi.length - 1)));
        }
        
        let segmentTerminator = edi.charAt(SEGMENT_TERMINATOR_POS);
        let elementDelimiter = edi.charAt(ELEMENT_DELIMITER_POS);
        
        if (edi.charAt(103) !== elementDelimiter) {
            let errorMessage = 'X12 Standard: The ISA segment is not the correct length (106 characters, including segment terminator).';
            
            if (this._strict) {
                throw new ParserError(errorMessage);
            }
            
            this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, new Range(0, 0, 0, 2)));
        }
        
        let interchange = new X12Interchange(segmentTerminator, elementDelimiter);
        let group: X12FunctionalGroup;
        let transaction: X12Transaction;
        
        let segments = this._parseSegments(edi, segmentTerminator, elementDelimiter);
        
        segments.forEach((seg) => {
            if (seg.tag == 'ISA') {
                this._processISA(interchange, seg);
            }
            
            else if (seg.tag == 'IEA') {
                this._processIEA(interchange, seg);
            }
            
            else if (seg.tag == 'GS') {
                group = new X12FunctionalGroup();
                
                this._processGS(group, seg);
                interchange.functionalGroups.push(group);
            }
            
            else if (seg.tag == 'GE') {
                if (!group) {
                    let errorMessage = 'X12 Standard: Missing GS segment!';
                    
                    if (this._strict) {
                        throw new ParserError(errorMessage);
                    }
                    
                    this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, seg.range));
                }
                
                this._processGE(group, seg);
                group = null;
            }
            
            else if (seg.tag == 'ST') {
                if (!group) {
                    let errorMessage = `X12 Standard: ${seg.tag} segment cannot appear outside of a functional group.`;
                    
                    if (this._strict) {
                        throw new ParserError(errorMessage);
                    }
                    
                    this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, seg.range));
                }
                
                transaction = new X12Transaction();
                
                this._processST(transaction, seg);
                group.transactions.push(transaction);
            }
            
            else if (seg.tag == 'SE') {
                if (!group) {
                    let errorMessage = `X12 Standard: ${seg.tag} segment cannot appear outside of a functional group.`;
                    
                    if (this._strict) {
                        throw new ParserError(errorMessage);
                    }
                    
                    this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, seg.range));
                }
                
                if (!transaction) {
                    let errorMessage = 'X12 Standard: Missing ST segment!';
                    
                    if (this._strict) {
                        throw new ParserError(errorMessage);
                    }
                    
                    this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, seg.range));
                }
                
                this._processSE(transaction, seg);
                transaction = null;
            }
            
            else {
                if (!group) {
                    let errorMessage = `X12 Standard: ${seg.tag} segment cannot appear outside of a functional group.`;
                    
                    if (this._strict) {
                        throw new ParserError(errorMessage);
                    }
                    
                    this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, seg.range));
                }
                
                if (!transaction) {
                    let errorMessage = `X12 Standard: ${seg.tag} segment cannot appear outside of a transaction.`;
                    
                    if (this._strict) {
                        throw new ParserError(errorMessage);
                    }
                    
                    this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, seg.range));
                }
                
                else {
                    transaction.segments.push(seg);
                }
            }
        });
        
        return interchange;
    }
    
    private _parseSegments(edi: string, segmentTerminator: string, elementDelimiter: string): X12Segment[] {
        let segments = new Array<X12Segment>();
        
        let tagged = false;
        let currentSegment: X12Segment;
        let currentElement: X12Element;
        
        currentSegment = new X12Segment();

        for (let i = 0, l = 0, c = 0; i < edi.length; i++) {
			// segment not yet named and not whitespace or delimiter - begin naming segment
			if (!tagged && (edi[i].search(/\s/) == -1) && (edi[i] !== elementDelimiter) && (edi[i] !== segmentTerminator)) {
				currentSegment.tag += edi[i];
				
                if (!currentSegment.range.start) {
                    currentSegment.range.start = new Position(l, c);
                }
			}
			
			// trailing line breaks - consume them and increment line number
			else if (!tagged && (edi[i].search(/\s/) > -1)) {
				if (edi[i] == '\n') {
					l++;
					c = -1;
				}
			}
			
			// segment tag/name is completed - mark as tagged
			else if (!tagged && (edi[i] == elementDelimiter)) {
				tagged = true;
				
				currentElement = new X12Element();
                currentElement.range.start = new Position(l, c);
			}
			
			// segment terminator
			else if (edi[i] == segmentTerminator) {
                currentElement.range.end = new Position(l, (c - 1));
                currentSegment.elements.push(currentElement);
                currentSegment.range.end = new Position(l, c);
                
				segments.push(currentSegment);
				
				currentSegment = new X12Segment();
				tagged = false;
                
                if (segmentTerminator === '\n') {
                    l++;
                    c = -1;
                }
			}
			
			// element delimiter
			else if (tagged && (edi[i] == elementDelimiter)) {
                currentElement.range.end = new Position(l, (c - 1));
				currentSegment.elements.push(currentElement);
				
				currentElement = new X12Element();
                currentElement.range.start = new Position(l, c + 1);
			}
			
			// element data
			else {
				currentElement.value += edi[i];
			}
			
			c++;
		}

        return segments;
    }
    
    private _processISA(interchange: X12Interchange, segment: X12Segment): void {
        interchange.header = segment;
    }
    
    private _processIEA(interchange: X12Interchange, segment: X12Segment): void {
        interchange.trailer = segment;

		if (parseInt(segment.valueOf(1)) !== interchange.functionalGroups.length) {
			let errorMessage = `X12 Standard: The value in IEA01 (${segment.valueOf(1)}) does not match the number of GS segments in the interchange (${interchange.functionalGroups.length}).`;
			
            if (this._strict) {
                throw new ParserError(errorMessage);
            }
            
            this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, segment.elements[0].range));
		}
				
		if (segment.valueOf(2) !== interchange.header.valueOf(13)) {
			let errorMessage = `X12 Standard: The value in IEA02 (${segment.valueOf(2)}) does not match the value in ISA13 (${interchange.header.valueOf(13)}).`;
			
            if (this._strict) {
                throw new ParserError(errorMessage);
            }
            
            this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, segment.elements[1].range));
		}
    }
    
    private _processGS(group: X12FunctionalGroup, segment: X12Segment): void {
        group.header = segment;
    }
    
    private _processGE(group: X12FunctionalGroup, segment: X12Segment): void {
        group.trailer = segment;
				
		if (parseInt(segment.valueOf(1)) !== group.transactions.length) {
			let errorMessage = `X12 Standard: The value in GE01 (${segment.valueOf(1)}) does not match the number of ST segments in the functional group (${group.transactions.length}).`;
			
            if (this._strict) {
                throw new ParserError(errorMessage);
            }
            
            this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, segment.elements[0].range));
		}
				
		if (segment.valueOf(2) !== group.header.valueOf(6)) {
			let errorMessage = `X12 Standard: The value in GE02 (${segment.valueOf(2)}) does not match the value in GS06 (${group.header.valueOf(6)}).`;
			
            if (this._strict) {
                throw new ParserError(errorMessage);
            }
            
            this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, segment.elements[1].range));
		}
    }
    
    private _processST(transaction: X12Transaction, segment: X12Segment): void {
        transaction.header = segment;
    }
    
    private _processSE(transaction: X12Transaction, segment: X12Segment): void {
        transaction.trailer = segment;
				
		let expectedNumberOfSegments = (transaction.segments.length + 2);
				
		if (parseInt(segment.valueOf(1)) !== expectedNumberOfSegments) {
			let errorMessage = `X12 Standard: The value in SE01 (${segment.valueOf(1)}) does not match the number of segments in the transaction (${expectedNumberOfSegments}).`;
			
            if (this._strict) {
                throw new ParserError(errorMessage);
            }
            
            this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, segment.elements[0].range));
		}
				
		if (segment.valueOf(2) !== transaction.header.valueOf(2)) {
			let errorMessage = `X12 Standard: The value in SE02 (${segment.valueOf(2)}) does not match the value in ST02 (${transaction.header.valueOf(2)}).`;
			
            if (this._strict) {
                throw new ParserError(errorMessage);
            }
            
            this.diagnostics.push(new X12Diagnostic(X12DiagnosticLevel.Error, errorMessage, segment.elements[1].range));
		}
    }
}