import { BCS, getSuiMoveConfig, toHEX, } from '@mysten/bcs';
import { SuiObjectResponse, DynamicFieldPage } from '@mysten/sui/client';
import { ERROR, Errors } from './exception';
import { isValidSuiAddress, normalizeSuiAddress} from '@mysten/sui/utils'
import { RepositoryValueType, ValueType, Protocol, ContextType, OperatorType } from './protocol'

export const MAX_U8 = BigInt('255');
export const MAX_U64 = BigInt('18446744073709551615');
export const MAX_U128 = BigInt('340282366920938463463374607431768211455');
export const MAX_U256 = BigInt('115792089237316195423570985008687907853269984665640564039457584007913129639935');

export const OPTION_NONE = 0;

export const ValueTypeConvert = (type:ValueType | null | undefined) : RepositoryValueType | number => {
    if (type === ValueType.TYPE_U8 || type === ValueType.TYPE_U64 || type === ValueType.TYPE_U128 || 
        type === ValueType.TYPE_U256) {
            return RepositoryValueType.PositiveNumber
    } else if (type === ValueType.TYPE_VEC_U8 || type === ValueType.TYPE_VEC_U64 || type === ValueType.TYPE_VEC_U128 || 
        type === ValueType.TYPE_VEC_U256|| type === ValueType.TYPE_VEC_BOOL) {
            return RepositoryValueType.PositiveNumber_Vec
    } else if (type === ValueType.TYPE_ADDRESS) {
        return RepositoryValueType.Address
    } else if (type === ValueType.TYPE_VEC_ADDRESS) {
        return RepositoryValueType.Address_Vec
    } else if (type === ValueType.TYPE_STRING) {
        return RepositoryValueType.String
    } else if (type === ValueType.TYPE_VEC_STRING) {
        return RepositoryValueType.String_Vec
    } else if (type === ValueType.TYPE_BOOL) {
        return RepositoryValueType.Bool
    }
    return -1;
}

export const readOption = (arr: number[], de:ValueType) : {bNone:boolean, value:any} => {
    let o = arr.splice(0, 1);
    if (o[0] == 1) { // true
        return {bNone:false,  value:Bcs.getInstance().de(de, Uint8Array.from(arr))};
    } else if (o[0] == 0) {
        return {bNone:true, value:OPTION_NONE};
    } else {
        ERROR(Errors.Fail, 'readOption: option invalid')
        return {bNone:true, value:OPTION_NONE}
    }
}

export const readOptionString = (arr: number[]) : {bNone:boolean, value:any}=> {
    let o = arr.splice(0, 1);
    if (o[0] == 1) { // true
        let r = ulebDecode(Uint8Array.from(arr));
        let value = Bcs.getInstance().de(ValueType.TYPE_STRING, Uint8Array.from(arr));
        arr.splice(0, r.value+r.length);
        return {bNone:false,  value:value};
    } else if (o[0] == 0) {
        return {bNone:true, value:OPTION_NONE};
    } else {
        ERROR(Errors.Fail, 'readOption: option invalid')
        return {bNone:true, value:OPTION_NONE}
    }
}

export const ulebDecode = (arr: Uint8Array) : {value: number, length: number} => {
	let total = 0;
	let shift = 0;
	let len = 0;

	// eslint-disable-next-line no-constant-condition
	while (true) {
		let byte = arr[len];
		len += 1;
		total |= (byte & 0x7f) << shift;
		if ((byte & 0x80) === 0) {
			break;
		}
		shift += 7;
	}

	return {
		value: total,
		length: len,
	};
}

export const readVec = (arr: any[], cb:(arr:any[], i:number, length:number) => any) : any[] => {
    let r = ulebDecode(Uint8Array.from(arr));
    arr.splice(0, r.length) ;

    let result = [];
    for (let i = 0; i < r.value; i++) {
        result.push(cb(arr, i, r.value));
    }    
    return result;
}

export const cb_U8 = (arr:any[], i:number, length:number) : any => {
    return arr.shift();
}
export const cb_U64 = (arr:any[], i:number, length:number) : any => {
    return arr.splice(0, 8);
}
export const cb_U128 = (arr:any[], i:number, length:number) : any => {
    return arr.splice(0, 16);
}
export const cb_U256 = (arr:any[], i:number, length:number) : any => {
    return arr.splice(0, 32);
}

export const concatenate = (resultConstructor:any, ...arrays:any[]) => {
    let totalLength = 0;
    for (const arr of arrays) {
        totalLength += arr.length;
    }
    const result = new resultConstructor(totalLength);
    let offset = 0;
    for (const arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}

export const parseObjectType = (chain_type:string | null | undefined, header:string='payment::Payment<') : string =>  {
    if (chain_type) {
        const i = chain_type.indexOf(header);
        if (i > 0) {
            let r = chain_type.slice(i + header.length, chain_type.length-1);
            return r
        }
    }
    return '';
}

export const array_equal =  (arr1: any[], arr2: any[]) => {
    if (arr1.length !== arr2.length) {
      return false;
    }
    return !arr1.some((item) => !arr2.includes(item));
}

export const array_unique = (arr:any[]) : any[] =>  {
    var newArr = [];
    for(var i = 0; i < arr.length; i++) {
        if(newArr.indexOf(arr[i]) == -1) {
            newArr.push(arr[i]);
        }
    }
    return newArr;
}

export function capitalize (s:string) : string { 
    return s && s[0].toUpperCase() + s.slice(1)
}

// for: "0xsdjfkskf<0x2::sui::coin<xxx>, 0xfdfff<>>"
export function parse_object_type(object_data:string) : string[] {
    var object_type : string[] = [];
    let type_pos = object_data.indexOf('<');
    if (type_pos >= 0) { 
        let t = object_data.slice((type_pos+1), object_data.length-1);
        object_type = t.split(',');
    }
    return object_type;
}

export class Bcs {
    bcs = new BCS(getSuiMoveConfig());
    private static _instance : any;
    private constructor() {
        this.bcs.registerEnumType('Option<T>', {
            'none': null,
            'some': 'T',
        });
        this.bcs.registerStructType('EntStruct', {
            'avatar': 'vector<u8>',
            'resource': "Option<address>",
            "safer_name": "vector<string>",
            "safer_value": "vector<string>",
            'like': BCS.U32,
            'dislike': BCS.U32,
        });
        this.bcs.registerStructType('TagStruct', {
            'nick': 'string',
            'tags': "vector<string>",
        })
        this.bcs.registerStructType('PersonalInfo', {
            'name': 'vector<u8>',
            'description': 'vector<u8>',
            'avatar': BCS.STRING,
            'twitter': BCS.STRING,
            'discord': BCS.STRING,
            'homepage': BCS.STRING,
        })
        this.bcs.registerStructType('OptionAddress', {
            'address': 'Option<address>',
        })
        this.bcs.registerStructType('Guards', {
            'guards':'vector<OptionAddress>',
        })
        this.bcs.registerStructType('Perm', {
            'index': BCS.U64,
            'guard': 'Option<address>'
        })
        this.bcs.registerStructType('Perms', {
            'perms':'vector<Perm>'
        })
    }
    static getInstance() : Bcs { 
        if (!Bcs._instance) {
            Bcs._instance =  new Bcs();
        };
        return Bcs._instance;
     }

    ser_option_u32(data:Uint8Array | any) : Uint8Array {
        return this.bcs.ser('Option<u32>', {'some': data}).toBytes();
    }

    ser(type:ValueType | ContextType | string, data:Uint8Array | any) : Uint8Array {
        if (typeof(type) === 'string') {
            return this.bcs.ser(type, data).toBytes();
        }

        switch(type) {
            case ValueType.TYPE_BOOL:
                return this.bcs.ser(BCS.BOOL, data).toBytes();
            case ValueType.TYPE_ADDRESS:
                return this.bcs.ser(BCS.ADDRESS, data).toBytes();
            case ValueType.TYPE_U64:
                return this.bcs.ser(BCS.U64, data).toBytes();
            case ValueType.TYPE_U8:
                return this.bcs.ser(BCS.U8, data).toBytes();
            case ValueType.TYPE_VEC_U8:
                return this.bcs.ser('vector<u8>', data).toBytes();
            case ValueType.TYPE_U128:
                return this.bcs.ser(BCS.U128, data).toBytes();
            case ValueType.TYPE_VEC_ADDRESS:
                return this.bcs.ser('vector<address>', data).toBytes();
            case ValueType.TYPE_VEC_BOOL:
                return this.bcs.ser('vector<bool>', data).toBytes();
            case ValueType.TYPE_VEC_VEC_U8:
                return this.bcs.ser('vector<vector<u8>>', data).toBytes();
            case ValueType.TYPE_VEC_U64:
                return this.bcs.ser('vector<u64>', data).toBytes();
            case ValueType.TYPE_VEC_U128:
                return this.bcs.ser('vector<u128>', data).toBytes();
            case ValueType.TYPE_OPTION_ADDRESS:
                return this.bcs.ser('Option<address>', {'some': data}).toBytes();
            case ValueType.TYPE_OPTION_BOOL:
                return this.bcs.ser('Option<bool>', {'some': data}).toBytes();
            case ValueType.TYPE_OPTION_U8:
                return this.bcs.ser('Option<u8>', {'some': data}).toBytes();
            case ValueType.TYPE_OPTION_U64:
                return this.bcs.ser('Option<u64>', {'some': data}).toBytes();
            case ValueType.TYPE_OPTION_U128:
                return this.bcs.ser('Option<u128>', {'some': data}).toBytes();
            case ValueType.TYPE_OPTION_U256:
                return this.bcs.ser('Option<u256>', {'some': data}).toBytes();
            case ValueType.TYPE_OPTION_STRING:
                return this.bcs.ser('Option<string>', {'some': data}).toBytes();
            case ValueType.TYPE_VEC_U256:
                return this.bcs.ser('vector<u256>', data).toBytes();
            case ValueType.TYPE_U256:
                return this.bcs.ser(BCS.U256, data).toBytes();
            case ValueType.TYPE_STRING:
                const d = new TextEncoder().encode(data);
                return this.bcs.ser('vector<u8>', d).toBytes();
            case ValueType.TYPE_VEC_STRING:
                return this.bcs.ser('vector<vector<u8>>', data.map((v:string)=>{return new TextEncoder().encode(v)})).toBytes();
            default:
                ERROR(Errors.bcsTypeInvalid, 'ser');
        }
        return new Uint8Array();
    }

    de(type:ValueType | string,  data:Uint8Array | any) : any {
        if (typeof(type) === 'string') {
            return this.bcs.de(type, data);
        }

        switch(type) {
            case ValueType.TYPE_BOOL:
                return this.bcs.de(BCS.BOOL, data);
            case ValueType.TYPE_ADDRESS:
                return this.bcs.de(BCS.ADDRESS, data);
            case ValueType.TYPE_U64:
                return this.bcs.de(BCS.U64, data);
            case ValueType.TYPE_U8:
                return this.bcs.de(BCS.U8, data);
            case ValueType.TYPE_VEC_U8:
                return this.bcs.de('vector<u8>', data);
            case ValueType.TYPE_U128:
                return this.bcs.de(BCS.U128, data);
            case ValueType.TYPE_VEC_ADDRESS:
                return this.bcs.de('vector<address>', data);
            case ValueType.TYPE_VEC_BOOL:
                return this.bcs.de('vector<bool>', data);
            case ValueType.TYPE_VEC_VEC_U8:
                return this.bcs.de('vector<vector<u8>>', data);
            case ValueType.TYPE_VEC_U64:
                return this.bcs.de('vector<u64>', data);
            case ValueType.TYPE_VEC_U128:
                return this.bcs.de('vector<u128>', data);
            case ValueType.TYPE_OPTION_ADDRESS:
                return this.bcs.de('Option<address>', data);
            case ValueType.TYPE_OPTION_BOOL:
                return this.bcs.de('Option<bool>', data);
            case ValueType.TYPE_OPTION_U8:
                return this.bcs.de('Option<u8>', data);
            case ValueType.TYPE_OPTION_U64:
                return this.bcs.de('Option<u64>', data);
            case ValueType.TYPE_OPTION_U128:
                return this.bcs.de('Option<u128>', data);
            case ValueType.TYPE_OPTION_U256:
                return this.bcs.de('Option<u256>', data);
            case ValueType.TYPE_OPTION_STRING:
                return this.bcs.de('Option<string>', data);
            case ValueType.TYPE_VEC_U256:
                return this.bcs.de('vector<u256>', data);
            case ValueType.TYPE_STRING:
                const r = new TextDecoder().decode(Uint8Array.from(this.bcs.de('vector<u8>', data)));
                return r
            case ValueType.TYPE_VEC_STRING:
                return this.bcs.de('vector<string>', data);
            case ValueType.TYPE_U256:
                return this.bcs.de(BCS.U256, data);
            default:
                ERROR(Errors.bcsTypeInvalid, 'de');
        }
    }

    de_ent(data:Uint8Array | undefined) : any {
        if (!data || data.length < 2) return ''
        const struct_vec = this.bcs.de('vector<u8>', data);
        return this.bcs.de('EntStruct', Uint8Array.from(struct_vec));
    }    
    de_entInfo(data:Uint8Array | undefined) : any {
        if (!data || data.length === 0) return undefined
        let r = this.bcs.de('PersonalInfo', data);
        r.name = new TextDecoder().decode(Uint8Array.from(r.name));
        r.description = new TextDecoder().decode(Uint8Array.from(r.description));
        return r
    }    
    de_tags(data:Uint8Array | undefined) : any {
        if (!data || data.length === 0) return ''
        const struct_vec = this.bcs.de('vector<u8>', data);
        return this.bcs.de('TagStruct', Uint8Array.from(struct_vec));
    }   
    de_perms(data:Uint8Array | undefined) : any {
        if (!data || data.length  < 1) return ''
        let r = this.bcs.de('Perms', data);
        return r.perms.map((v:any) => {
            return {index: v?.index, guard:v?.guard?.none ? undefined : '0x'+v?.guard?.some}
        })
    }   
}

export function stringToUint8Array(str:string) {
    const encoder = new TextEncoder();
    const view = encoder.encode(str);
    return new Uint8Array(view.buffer);
}

export function numToUint8Array(num:number) : Uint8Array {
    if (!num) return new Uint8Array(0)
    const a = [];
    a.unshift(num & 255)
    while (num >= 256) {
        num = num >>> 8
        a.unshift(num & 255)
    }
    return new Uint8Array(a)
} 

export const isArr = (origin: any): boolean => {
    let str = '[object Array]'
    return Object.prototype.toString.call(origin) == str ? true : false
}


export const deepClone = <T>(origin: T, target?: Record<string, any> | T ): T => {
    let tar = target || {}

    for (const key in origin) {
        if (Object.prototype.hasOwnProperty.call(origin, key)) {
            if (typeof origin[key] === 'object' && origin[key] !== null) {
                tar[key] = isArr(origin[key]) ? [] : {}
                deepClone(origin[key], tar[key])
            } else {
                tar[key] = origin[key]
            }
        }
    }

    return tar as T
}

export const MAX_DESCRIPTION_LENGTH = 4000;
export const MAX_NAME_LENGTH = 64;
export const MAX_ENDPOINT_LENGTH = 1024;
// export const OptionNone = (txb:TransactionBlock) : TransactionArgument => { return txb.pure([], BCS.U8) };
const IsValidStringLength = (str: string, max_len:number) : boolean => {
    return Bcs.getInstance().ser(ValueType.TYPE_STRING, str).length <= max_len
}
export const IsValidDesription = (description:string) : boolean => { 
    return IsValidStringLength(description, MAX_DESCRIPTION_LENGTH)
} 
export const IsValidName = (name?:string) : boolean => { 
    if(!name || name.length === 0) {
        return false;
    } 
    return IsValidStringLength(name, MAX_NAME_LENGTH) 
}

export const IsValidName_AllowEmpty = (name:string) : boolean => { return IsValidStringLength(name, MAX_NAME_LENGTH)}
export const IsValidEndpoint = (endpoint:string) : boolean => { 
    return (endpoint.length > 0 && endpoint.length <= MAX_ENDPOINT_LENGTH && isValidHttpUrl(endpoint)) ;
}
export const IsValidAddress = (addr:string | undefined) : boolean => { 
    if (!addr || !isValidSuiAddress(addr)) {
        return false; 
    }
    return true
}
export const IsValidCoinType = (coin_type:string | undefined) : boolean => { 
    if (!coin_type) {
        return false; 
    }
    return coin_type.startsWith('0x2::coin::Coin') || coin_type.startsWith('0x0000000000000000000000000000000000000000000000000000000000000002')
}


export const getUTCDayStartByDivision = (interval=86400000): number => { // 1 day default
    const now = Date.now(); 
    return Math.floor(now / interval) * interval;
}

export const IsValidBigint = (value:string | number | undefined | bigint, max:bigint=MAX_U256, min?:bigint) : boolean => {
    if (value === '' || value === undefined) return false;
    try {
        const v = BigInt(value);
        if (v <= max) {
            if (min !== undefined) {
                return v >= min;
            }
            return true   
        }
    } catch (e) {
    }; return false
}

export const IsValidU8 = (value:string | number | undefined | bigint, min=0) : boolean => {
    return IsValidBigint(value, MAX_U8, BigInt(min))
}
export const IsValidU64 = (value:string | number | undefined | bigint, min=0) : boolean => {
    return IsValidBigint(value, MAX_U64, BigInt(min))
}
export const IsValidU128 = (value:string | number | undefined | bigint, min=0) : boolean => {
    return IsValidBigint(value, MAX_U128, BigInt(min))
}
export const IsValidU256 = (value:string | number | undefined | bigint, min=0) : boolean => {
    return IsValidBigint(value, MAX_U256, BigInt(min))
}

export const IsValidTokenType = (argType: string) : boolean => { 
    if (!argType || argType.length === 0) {
        return false; 
    }
    let arr = argType.split('::');
    if (arr.length !== 3) {
        return false;
    } 
    if ((!IsValidAddress(arr[0]) && arr[0] != '0x2') || arr[1].length === 0 || arr[2].length === 0) {
        return false;
    }
    return true;
}
export const IsValidArgType = (argType: string) : boolean => { 
    if (!argType || argType.length === 0) {
        return false; 
    }
    let arr = argType.split('::');
    if (arr.length < 3) {
        return false;
    } 
    return true;
}
export const IsValidInt = (value: number | string) : boolean => { 
    if (typeof(value) === 'string') {
        value = parseInt(value as string);
    }
    return Number.isSafeInteger(value) 
}
export const IsValidPercent = (value: number | string | bigint) : boolean => { 
    return IsValidBigint(value, BigInt(100), BigInt(0))
}

export const IsValidArray = (arr: any, validFunc:any) : boolean => {
    for (let i = 0; i < arr.length; i++) {
        if (!validFunc(arr[i])) {
            return false
        }
    }
    return true
}

export const ResolveU64 = (value:bigint) : bigint => {
    const max = MAX_U64;
    if (value > max) {
        return max;
    } else {
        return value
    }
}

function removeTrailingZeros(numberString: string): string {
    const trimmedString = numberString.trim();
    const decimalIndex = trimmedString.indexOf('.');
    
    if (decimalIndex !== -1) {
      let endIndex = trimmedString.length - 1;
      
      while (trimmedString[endIndex] === '0') {
        endIndex--;
      }
      
      if (trimmedString[endIndex] === '.') {
        endIndex--; 
      }
      
      return trimmedString.slice(0, endIndex + 1);
    }
    
    return trimmedString;
  }

export const ResolveBalance = (balance:string, decimals:number) : string => {
    if (!balance) return ''
    if (balance === '0') return '0'
    if (decimals <= 0) return balance;
    var pos = decimals - balance.length;
    if (pos === 0) {
       return removeTrailingZeros('.' + (balance));
    } else if (pos < 0) {
        let start = balance.slice(0, Math.abs(pos));
        let end = balance.slice(Math.abs(pos));
        return removeTrailingZeros(start + '.' + end);
    } else {
        return removeTrailingZeros('.' + balance.padStart(decimals, '0'));
    }
}

export type ArgType = {
    isCoin: boolean;
    coin: string;
    token: string;
}
export const ParseType = (type:string) : ArgType => {
    if (type) {
        const COIN = '0x2::coin::Coin<';
        let i = type.indexOf(COIN);
        if (i >= 0) {
            let coin = type.slice(i+COIN.length, type.length-1);
            if (coin.indexOf('<') === -1) {
                while (coin[coin.length-1] == '>') {
                    coin = coin.slice(0, -1);
                };
                let t = coin.lastIndexOf('::');      
                return {isCoin:true, coin:coin, token:coin.slice(t+2)}         
            }
        }
    }
    return  {isCoin:false, coin:'', token:''}
}


export function insertAtHead(array:Uint8Array, value:number) {
    const newLength = array.length + 1;
    const newArray = new Uint8Array(newLength);
    newArray.set([value], 0); 
    newArray.set(array, 1);
    return newArray;
}

export function toFixed(x:number) {
    let res = '';
    if (Math.abs(x) < 1.0) {
      var e = parseInt(x.toString().split('e-')[1]);
      if (e) {
          x *= Math.pow(10,e-1);
          res = '0.' + (new Array(e)).join('0') + x.toString().substring(2);
      }
    } else {
      var e = parseInt(x.toString().split('+')[1]);
      if (e > 20) {
          e -= 20;
          x /= Math.pow(10,e);
          res = x + (new Array(e+1)).join('0');
      }
    }
    return res;
  }

export function isValidHttpUrl(url:string) : boolean {
    let r:any;
    try {
      r = new URL(url);
    } catch (_) {
      return false;  
    }
  
    return r.protocol === "http:" || r.protocol === "https:" || r.protocol === 'ipfs:';
}

export interface query_object_param {
    id:string;
    onBegin?:(id:string)=>void;
    onObjectRes?:(id:string, res:SuiObjectResponse)=>void;
    onDynamicRes?:(id:string, res:DynamicFieldPage)=>void;
    onFieldsRes?:(id:string, fields_res:SuiObjectResponse[])=>void;
    onObjectErr?:(id:string, err:any)=>void;
    onDynamicErr?:(id:string, err:any)=>void;
    onFieldsErr?:(id:string, err:any)=>void;
}

export const uint2address = (value: number | bigint) : string => {
    return normalizeSuiAddress(value.toString(16)); 
}

export const query_object = (param:query_object_param) => {
    if (param.id) {
      if(param?.onBegin) param.onBegin(param.id);
      Protocol.Client().getObject({id:param.id, options:{showContent:true, showType:true, showOwner:true}}).then((res) => {
        if (res.error) {
            if(param?.onObjectErr) param.onObjectErr(param.id, res.error);
        } else {
            if(param?.onObjectRes) param.onObjectRes(param.id, res);
        }
      }).catch((err) => {
        console.log(err)
        if (param?.onObjectErr) param.onObjectErr(param.id, err);
      });

      Protocol.Client().getDynamicFields({parentId:param.id}).then((res) => {
        if (param?.onDynamicRes) param.onDynamicRes(param.id, res);
        
        if (res.data.length > 0) {
          Protocol.Client().multiGetObjects({ids:res.data.map(v => v.objectId), options:{showContent:true}}).then((fields) => {
            if (param?.onFieldsRes) param.onFieldsRes(param.id, fields);
          }).catch((err) => {
            console.log(err)
            if (param?.onFieldsErr) param.onFieldsErr(param.id, err);
          })          
        } 
      }).catch((err) => {
        console.log(err)
        if (param?.onDynamicErr) param.onDynamicErr(param.id, err);
      })
    }
  }

export const FirstLetterUppercase = (str:string|undefined|null) : string => {
    if (!str) return '';
    return str.substring(0, 1).toUpperCase() + str.substring(1);
}

  
export function hasDuplicates<T>(array: T[]): boolean {
    return array.some((item, index) => array.findIndex(i => i === item) !== index);
}