import {Injectable} from '@angular/core';
import {EthService} from '../eth.service';
import {BehaviorSubject} from 'rxjs';
import {filter} from 'rxjs/operators';

const abi = require('../../assets/contract/RandomGuess.json');

export enum RandomGameStatus {
  Initial,
  WaitingForPartnerRegistration,
  WaitBeforeSendNum,
  ReadyToSendNum,
  WaitForPartnerToSendNum,
  GettingWinner,
  FinishedAsLooser,
  ReadyToTakePrice,
  FinishedAsWinner,
}

export interface RandomGameState {
  status: RandomGameStatus;
  gameId?: number;
  num?: number;
  retry: number;
}

@Injectable({
  providedIn: 'root'
})
export class RandomGameService {
  private contract;
  private account;

  private stateSubject = new BehaviorSubject<RandomGameState>(this.initialState);
  state$ = this.stateSubject.asObservable();

  private get initialState() {
    return {...{status: RandomGameStatus.Initial, retry: 0}};
  }

  get state() {
    return this.stateSubject.value;
  }

  constructor(private ethService: EthService) {
    ethService.validProviderAndAccount$.subscribe(() => this.loadContract());

    this.stateSubject.pipe(
      filter(s => s.status === RandomGameStatus.WaitingForPartnerRegistration),
    ).subscribe(s => this.waitForPartnerRegistration(s.gameId));
    this.stateSubject.pipe(
      filter(s =>
        s.status === RandomGameStatus.WaitBeforeSendNum
      ),
    ).subscribe(() => {
      this.extendStateIfGameIdAndStateIs(this.state.gameId, RandomGameStatus.WaitBeforeSendNum, {
        status: RandomGameStatus.ReadyToSendNum,
      });
    });
    this.stateSubject.pipe(
      filter(s => s.status === RandomGameStatus.ReadyToSendNum),
    ).subscribe(s => this.submitGuess(s.gameId, s.num));
    this.stateSubject.pipe(
      filter(s => s.status === RandomGameStatus.WaitForPartnerToSendNum),
    ).subscribe(s => this.waitForPartnerSubmitResult(s.gameId));
    this.stateSubject.pipe(
      filter(s => s.status === RandomGameStatus.GettingWinner),
    ).subscribe(s => this.checkWinner(s.gameId));
    this.stateSubject.pipe(
      filter(s => s.status === RandomGameStatus.ReadyToTakePrice),
    ).subscribe(s => this.takePrice(s.gameId));
  }

  async deploy() {
    return await this.contract.deploy({
      data: ' ',
    })
      .send({
        from: await this.ethService.getDefaultAccount(),
        gas: 1500000,
        gasPrice: '1000000000'
      })
      .on('error', console.error)
      .on('transactionHash', function(transactionHash) {
        console.log(`transaction hash: `, transactionHash);
      })
      .on('receipt', function(receipt) {
        console.log('the new contract address: ', receipt.contractAddress);
      })
      .catch(e => console.error(`failed to receipt  new contract address:`, e))
      // .on('confirmation', function (confirmationNumber, receipt) {
      //   console.log('confirmation', confirmationNumber, receipt)
      // })
      .then(function(newContractInstance) {
        console.log('new contract', newContractInstance.options.address); // instance with the new contract address
        return newContractInstance;
      });
  }

  private get eth() {
    return this.ethService.eth;
  }

  private get web3() {
    return this.ethService.web3;
  }

  retry() {
    this.extendState({retry: this.state.retry + 1}, false);
  }

  private extendStateIfGameIdAndStateIs(gameId: number, status: RandomGameStatus, fields: Partial<RandomGameState>) {
    if (this.state.status === status && this.state.gameId === gameId) {
      console.log('game state is changing...', fields);
      this.extendState(fields);
    } else {
      console.warn(`tried to extend state, but precondition failed`);
    }
  }

  private extendState(fields: Partial<RandomGameState>, resetRetry = true) {
    fields.retry = 0;
    this.stateSubject.next({...this.stateSubject.value, ...fields});
  }


  private async loadContract() {
    this.account = await this.ethService.getDefaultAccount();
    const address = this.ethService.contractAddresses.RandomGuess;
    this.contract = new this.eth.Contract(abi, address, {
      from: this.account,
      gas: 5000000,
      gasPrice: '100000000000',
    });
  }

  async sendGameTransaction(valHash: string, gas) {
    return await new Promise<string>((resolve, reject) => this.contract.methods.register(valHash).send({value: 2, gas})
      .on('transactionHash', hash => {
        console.log('transactionHash', hash);
        this.eth.getTransactionReceipt(hash).then(ret => {
          console.log('ret', ret);
          // if (ret && ret.logs.length === 1) { // todo only locally??? every second time???
          //   resolve(ret.logs[0].data);
          // } else {
          this.ethService.waitForReceipt(hash).then(res => {
            console.log('waitForReceipt', res);
            this.contract.methods.getMyGame().call().then(value => resolve(value));
          });
          // }
        });
      })
      .on('error', (error, receipt, confirmations) => { // https://github.com/ethereum/web3.js/issues/1859#issuecomment-484051789
        console.log('error', error, receipt, confirmations);
      })
      .on('receipt', (receipt) => {
        console.log('receipt');
      })
      .on('confirmation', (confirmationNumber, receipt) => {
        console.log('confirmation', confirmationNumber, receipt);
      })
      .catch(e => {
        console.error('failed to register', e);
        reject(e);
      })
      .then(value => console.log('got then answer:', value))
    );
  }

  async registerForGame(val: number) {
    console.log('generate hash...');
    const hash = await this.contract.methods.computeSecret(val).call();

    console.log('unlock account');
    await this.ethService.unlockAccount();

    console.log(`register game for ${this.account}...`);
    const gas = await new Promise<number>((resolve, reject) =>
      this.contract.methods.register(hash).estimateGas({gas: 5000000, value: 2}, (error, gasAmount) => {
        if (gasAmount === 5000000) {
          console.log('Method ran out of gas');
          reject();
        }
        console.log('gas:', gasAmount);
        resolve(gasAmount);
      }));
    const gameStr = await this.sendGameTransaction(hash, gas + 5000);
    return parseInt(gameStr, 10);
  }

  get canStartGame() {
    return this.state.status === RandomGameStatus.Initial;
  }

  async startGame(val: number) {
    if (!this.canStartGame) {
      throw new Error('not ready to start new game');
    }
    if (!(val > 0 && val <= 100)) {
      throw new Error('value not in range');
    }

    const gameId = await this.registerForGame(val);
    console.log('done, game id=', gameId);

    this.extendStateIfGameIdAndStateIs(undefined, RandomGameStatus.Initial, {
      gameId,
      status: RandomGameStatus.WaitingForPartnerRegistration,
      num: val,
    });
    return gameId;
  }

  get canReset() {
    return !this.canStartGame;
  }

  resetGame() {
    if (!this.canReset) {
      throw new Error('not ready to reset game');
    }
    this.stateSubject.next(this.initialState);
  }

  private waitForPartnerRegistration(gameId: number) {
    this.contract.events.RevalReady({
      filter: {game_id: gameId},
    }).on('data', () => this.extendStateIfGameIdAndStateIs(gameId, RandomGameStatus.WaitingForPartnerRegistration, {
      status: RandomGameStatus.WaitBeforeSendNum
    }));
    // may we are too late to get the event ...
    this.contract.methods.isRevalReady(gameId).call().then(isReady => {
      console.log('partner sumbitted?', isReady);
      if (isReady) {
        this.extendStateIfGameIdAndStateIs(gameId, RandomGameStatus.WaitingForPartnerRegistration, {
          status: RandomGameStatus.WaitBeforeSendNum
        });
      }
    });
  }

  private async submitGuess(gameId: number, num: number) {
    const gas = await new Promise<number>((resolve, reject) =>
      this.contract.methods.submitReval(gameId, num).estimateGas({gas: 5000000}, (error, gasAmount) => {
        if (error || gasAmount === 5000000) {
          console.log('Method ran out of gas', error);
          reject();
        } else {
          console.log('gas:', gasAmount);
          resolve(gasAmount);
        }
      }));
    await this.ethService.unlockAccount();
    console.log('going to submitReval...');
    this.contract.methods.submitReval(gameId, num).send({gas: gas + 5000})
      .on('transactionHash', hash => {
        console.log('got hash of submitReval', hash);
        this.ethService.waitForReceipt(hash).then(res => {
          console.log('submitReval res', res);
          this.extendStateIfGameIdAndStateIs(gameId, RandomGameStatus.ReadyToSendNum, {
            status: RandomGameStatus.WaitForPartnerToSendNum
          });
        });
      }).on('confirmation', confNumber => {
      console.log('confirmed submitReval', confNumber);
      this.extendStateIfGameIdAndStateIs(gameId, RandomGameStatus.ReadyToSendNum, {
        status: RandomGameStatus.WaitForPartnerToSendNum
      });
    }).on('error', error => console.log('erroor submitReval', error));
  }

  private checkWinner(gameId: number) {
    this.contract.methods.isWinner(gameId).call().then(isWinner => {
      if (isWinner) {
        this.extendStateIfGameIdAndStateIs(gameId, RandomGameStatus.GettingWinner, {
          status: RandomGameStatus.ReadyToTakePrice
        });
      } else {
        this.extendStateIfGameIdAndStateIs(gameId, RandomGameStatus.GettingWinner, {
          status: RandomGameStatus.FinishedAsLooser
        });
      }
    });
  }

  private waitForPartnerSubmitResult(gameId: number) {
    this.contract.events.PayoutReady({
      filter: {game_id: gameId},
    }).on('data', () => this.extendStateIfGameIdAndStateIs(gameId, RandomGameStatus.WaitForPartnerToSendNum, {
      status: RandomGameStatus.GettingWinner
    }));
    // may we are too late to get the event ...
    this.contract.methods.isPayoutReady(gameId).call().then(isReady => {
      console.log('partner submitted?', isReady);
      if (isReady) {
        this.extendStateIfGameIdAndStateIs(gameId, RandomGameStatus.WaitForPartnerToSendNum, {
          status: RandomGameStatus.GettingWinner
        });
      }
    });
  }

  private takePrice(gameId: number) {
    this.contract.methods.sendMoney(gameId).send().on('transactionHash', hash => {
      console.log('got hash', hash);
      this.eth.getTransaction(hash).then(res => {
        console.log(res);
        this.extendStateIfGameIdAndStateIs(gameId, RandomGameStatus.ReadyToTakePrice, {
          status: RandomGameStatus.FinishedAsWinner
        });
      });
    });
  }
}
