import { Iterable , Map as ImmutableMap } from 'immutable' ; import { FormException } from './form-exception' ; export interface Operations < T > { clone ( ) : T ; merge ( key , value ) : any ; update ( key , value ) : any ; } export interface TraverseCallback { ( parent , key , remainingPath , value ? ) : any ; } export abstract class State { static traverse < State > ( state , path , fn ? ) { let deepValue = state ; for ( const k of path ) { const parent = deepValue ; if ( Iterable . isIterable ( deepValue ) ) { const m = < ImmutableMap < string , any > > < any > deepValue ; if ( typeof m . get === 'function' ) { deepValue = m . get ( k ) ; } else { throw new FormException ( `Cannot retrieve value from immutable nonassociative container: ${ k }` ) ; } } else if ( deepValue instanceof Map ) { deepValue = ( < Map < string , any > > < any > deepValue ) . get ( k ) ; } else { deepValue = ( deepValue as any ) [ k ] ; } if ( typeof fn === 'function' ) { const transformed = fn ( parent , k , path . slice ( path . indexOf ( k ) + 1 ) , deepValue ) ; deepValue = transformed [ k ] ; Object . assign ( parent , transformed ) ; } if ( deepValue === undefined ) { return undefined ; } } return deepValue ; } static get < State > ( state , path ) { return State . traverse ( state , path ) ; } static assign < State > ( state , path , value ? ) { const operations = State . inspect ( state ) ; if ( path . length === 0 ) { return operations . update ( null , value ) ; } const root = operations . clone ( ) ; State . traverse ( root , path , ( parent , key , remainingPath , innerValue ? ) => { const parentOperations = State . inspect ( parent ) ; if ( innerValue ) { const innerOperations = State . inspect ( innerValue ) ; return parentOperations . update ( key , remainingPath . length > 0 ? innerOperations . clone ( ) : innerOperations . merge ( null , value ) ) ; } else { const getProbableType = ( key ) => { return typeof key === 'number' ? new Array ( ) : Array . isArray ( key ) ? ImmutableMap ( ) : new Object ( ) ; } ; return parentOperations . update ( key , remainingPath . length > 0 ? getProbableType ( remainingPath [ 0 ] ) : value ) ; } } ) ; return root ; } static inspect < K > ( object ) { const metaOperations = ( update , merge , clone ? ) => { const operations = { clone : typeof clone === 'function' ? ( ) => clone ( < any > object ) as any : ( ) => object , update : ( key , value ) => update ( operations . clone ( ) , key , value ) , merge : ( key , value ) => { const cloned = operations . clone ( ) ; return merge ( cloned , key , value , ( v ) => update ( cloned , key , v ) ) ; } } ; return operations ; } ; if ( Iterable . isIterable ( object ) ) { return metaOperations ( ( parent , key , value ) => { if ( key != null ) { return parent . set ( key , value ) ; } else { return value ; } } , ( parent , key , value ) => { if ( key ) { return parent . mergeDeepIn ( Array . isArray ( key ) ? key : [ key ] , value ) ; } else { if ( ImmutableMap . isMap ( value ) ) { return parent . mergeDeep ( value ) ; } else { return parent . concat ( value ) ; } } } ) ; } else if ( Array . isArray ( object ) ) { return metaOperations ( ( parent , key , value ) => { if ( key != null ) { parent [ key ] = value ; } else { parent . splice . apply ( parent , [ 0 , parent . length ] . concat ( Array . isArray ( value ) ? value : [ value ] ) ) ; } } , ( parent , _ , value , setter ) => { setter ( parent . concat ( value ) ) ; return parent ; } , ( ) => Array . prototype . slice . call ( object , 0 ) ) ; } else if ( object instanceof Map ) { return metaOperations ( ( parent , key , value ) => { if ( key != null ) { return parent . set ( key , value ) ; } else { const m = new Map ( < any > value ) ; parent . clear ( ) ; m . forEach ( ( value , index ) => parent . set ( index , value ) ) ; return parent ; } } , ( parent , _ , value ) => { const m = new Map < string , any > ( < any > value ) ; m . forEach ( ( value , key ) => parent . set ( key , value ) ) ; return parent ; } , ( ) => object instanceof WeakMap ? new WeakMap < Object , any > ( < any > object ) : new Map < string , any > ( < any > object ) ) ; } else if ( object instanceof WeakSet || object instanceof Set ) { return metaOperations ( ( parent , key , value ) => { if ( key != null ) { return parent . set ( key , value ) ; } else { const s = new Set ( < any > value ) ; s . forEach ( ( value , index ) => parent . set ( index , value ) ) ; s . clear ( ) ; return parent ; } } , ( parent , _ , value ) => { for ( const element of value ) { parent . add ( element ) ; } return parent ; } , ( ) => object instanceof WeakSet ? new WeakSet < any > ( < any > object ) : new Set < any > ( < any > object ) ) ; } else if ( object instanceof Date ) { throw new FormException ( 'Cannot understand why a Date object appears in the mutation path!' ) ; } else { switch ( typeof object ) { case 'boolean' : case 'function' : case 'number' : case 'string' : case 'symbol' : case 'undefined' : break ; case 'object' : if ( object == null ) { break ; } return metaOperations ( ( parent , key , value ) => { if ( key != null ) { return Object . assign ( parent , { [ key ] : value } ) ; } return Object . assign ( parent , value ) ; } , ( parent , _ , value ) => { for ( const k of Object . keys ( value ) ) { parent [ k ] = ( value as any ) [ k ] ; } return parent ; } , ( ) => Object . assign ( { } , object ) ) ; default : break ; } } throw new Error ( `An object of type ${ typeof object } has appeared in the mutation path! Every element ` + 'in the mutation path should be an array, an associative container, or a set' ) ; } static empty ( value ) { return value == null || ( value . length === 0 || ( typeof value . length === 'undefined' && Object . keys ( value ) . length === 0 ) ) ; } }