content: /*
   * Copyright 2019 ACINQ SAS
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package fr.acinq.eclair.wallet.fragments.openchannel;
  
  import android.content.Context;
  import android.content.SharedPreferences;
  import android.os.Bundle;
  import android.preference.PreferenceManager;
  import android.view.LayoutInflater;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.inputmethod.InputMethodManager;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.databinding.DataBindingUtil;
  import androidx.fragment.app.Fragment;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import fr.acinq.bitcoin.Btc;
  import fr.acinq.bitcoin.MilliBtc;
  import fr.acinq.bitcoin.Satoshi;
  import fr.acinq.eclair.CoinUnit;
  import fr.acinq.eclair.CoinUtils;
  import fr.acinq.eclair.channel.Channel;
  import fr.acinq.eclair.io.NodeURI;
  import fr.acinq.eclair.wallet.App;
  import fr.acinq.eclair.wallet.R;
  import fr.acinq.eclair.wallet.activities.OpenChannelActivity;
  import fr.acinq.eclair.wallet.databinding.FragmentOpenChannelCapacityBinding;
  import fr.acinq.eclair.wallet.utils.Constants;
  import fr.acinq.eclair.wallet.utils.TechnicalHelper;
  import fr.acinq.eclair.wallet.utils.WalletUtils;
  import scala.math.BigDecimal;
  
  public class OpenChannelCapacityFragment extends Fragment {
  
    private final Logger log = LoggerFactory.getLogger(OpenChannelCapacityFragment.class);
    private NodeURI nodeURI;
    public FragmentOpenChannelCapacityBinding mBinding;
  
    public void setNodeURI(final NodeURI uri) {
      this.nodeURI = uri;
    }
  
    OnCapacityConfirmListener mCallback;
  
    public void setListener(OpenChannelActivity activity) {
      mCallback = activity;
    }
  
    public interface OnCapacityConfirmListener {
      void onCapacityConfirm(final Satoshi capacity, final long feesSatPerKW, final boolean requireLiquidity);
  
      void onCapacityBack();
    }
  
    final Satoshi minFunding = MilliBtc.apply(BigDecimal.exact(1)).toSatoshi();
    final Satoshi maxFunding = Btc.apply(BigDecimal.exact(0.5)).toSatoshi();
    private int feeRatingState = Constants.FEE_RATING_FAST;
    private String preferredFiatCurrency = Constants.FIAT_USD;
    private CoinUnit preferredBitcoinUnit = CoinUtils.getUnitFromString(Constants.BTC_CODE);
  
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setHasOptionsMenu(false);
    }
  
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
      mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_open_channel_capacity, container, false);
  
      final SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getContext());
      preferredFiatCurrency = WalletUtils.getPreferredFiat(sharedPrefs);
      preferredBitcoinUnit = WalletUtils.getPreferredCoinUnit(sharedPrefs);
      setFeesToDefault();
      mBinding.setNode(this.nodeURI);
  
      mBinding.requestLiquidityCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
        mBinding.setGoToLiquidity(isChecked);
      });
      mBinding.capacityLayout.setOnClickListener(v -> focusField(mBinding.capacityValue));
      mBinding.fundingFeesLayout.setOnClickListener(v -> focusField(mBinding.fundingFeesValue));
      mBinding.fundingFeesRating.setOnClickListener(v -> pickFees());
      mBinding.buttonCancel.setOnClickListener(v -> mCallback.onCapacityBack());
      mBinding.buttonNext.setOnClickListener(v -> confirmOpenChannel());
      mBinding.useAllFundsCheckbox.setOnCheckedChangeListener((v, isChecked) -> {
        if (isChecked) {
          useAllAvailableFunds();
        } else {
          mBinding.capacityValue.setEnabled(true);
        }
      });
  
      mBinding.capacityValue.addTextChangedListener(new TechnicalHelper.SimpleTextWatcher() {
        public void onTextChanged(CharSequence s, int start, int before, int count) {
          // toggle hint depending on amount input
          mBinding.capacityHint.setVisibility(s == null || s.length() == 0 ? View.VISIBLE : View.GONE);
          extractCapacity(s.toString());
        }
      });
      mBinding.fundingFeesValue.addTextChangedListener(new TechnicalHelper.SimpleTextWatcher() {
        @Override
        public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
          extractFundingFees(s.toString());
        }
      });
      mBinding.capacityUnit.setText(preferredBitcoinUnit.shortLabel());
      return mBinding.getRoot();
    }
  
    /**
     * Reads the fees input by the user and return the value as a sat per kw Long. If the value is invalid, an error
     * message is displayed and this method returns null.
     *
     * @param feesString user input
     * @return null if invalid, Long in satoshi per kw otherwise
     */
    private Long extractFundingFees(final String feesString) {
      mBinding.setFeesWarning(null);
      mBinding.setFeesError(null);
      // fee value changes must invalidate the 'set all available funds' checkbox, since this amount
      // would probably not be correct anymore
      mBinding.useAllFundsCheckbox.setChecked(false);
      try {
        final long feesSatPerByte = Long.parseLong(feesString);
        if (feesSatPerByte != this.getApp().estimateSlowFees() && feesSatPerByte != this.getApp().estimateMediumFees() && feesSatPerByte != this.getApp().estimateFastFees()) {
          feeRatingState = Constants.FEE_RATING_CUSTOM;
          mBinding.setFeeRatingState(feeRatingState);
          mBinding.fundingFeesRating.setText(R.string.payment_fees_custom);
        }
        if (feesSatPerByte <= this.getApp().estimateSlowFees() / 2) {
          mBinding.setFeesWarning(getString(R.string.payment_fees_verylow));
        } else if (feesSatPerByte >= this.getApp().estimateFastFees() * 2) {
          mBinding.setFeesWarning(getString(R.string.payment_fees_veryhigh));
        } else {
          mBinding.setFeesWarning(null);
        }
        return fr.acinq.eclair.package$.MODULE$.feerateByte2Kw(feesSatPerByte);
      } catch (Throwable t) {
        log.debug("could not read fees with cause {}" + t.getMessage());
        mBinding.setFeesWarning(null);
        mBinding.setFeesError(getString(R.string.openchannel_error_fees_invalid));
        return null;
      }
    }
  
    /**
     * Checks if the String amount respects the following rules:
     * <ul>
     * <li>numeric</li>
     * <li>convertible to MilliSatoshi in the current user preferred unit</li>
     * <li>exceeds the minimal capacity amount (1mBTC)</li>
     * <li>does not exceed the maximal capacity amount (167 mBTC)</li>
     * <li>does not exceed the available onchain balance (confirmed + unconfirmed), accounting a minimal required leftover</li>
     * </ul>
     * <p>
     * If one of these rules is not respected, an error message will be added to the display binding and this method will return null.
     *
     * @param amount String amount
     * @return Millisatoshi object if amount is valid, null otherwise
     */
    private Satoshi extractCapacity(final String amount) {
      mBinding.setAmountError(null);
      try {
        final Satoshi capacity = CoinUtils.convertStringAmountToSat(amount, preferredBitcoinUnit.code());
        mBinding.capacityFiat.setText(getString(R.string.amount_to_fiat, WalletUtils.formatSatToFiatWithUnit(capacity, preferredFiatCurrency)));
        if (capacity.$less(minFunding) || capacity.$greater(maxFunding)) {
          mBinding.setAmountError(getString(R.string.openchannel_capacity_invalid, CoinUtils.formatAmountInUnit(minFunding, preferredBitcoinUnit, false),
            CoinUtils.formatAmountInUnit(maxFunding, preferredBitcoinUnit, true)));
          return null;
        } else if (getApp() != null && capacity.$greater(getApp().getOnchainBalance())) {
          mBinding.setAmountError(getString(R.string.openchannel_capacity_notenoughfunds));
          return null;
        } else {
          mBinding.setAmountError(null);
          return capacity;
        }
      } catch (Throwable t) {
        log.debug("could not convert capacity with cause {}", t.getMessage());
        mBinding.setAmountError(getString(R.string.openchannel_error_capacity_nan));
        return null;
      }
    }
  
    private void focusField(final View v) {
      v.requestFocus();
      if (getActivity() != null) {
        final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null) {
          imm.showSoftInput(v, 0);
        }
      }
    }
  
    private void useAllAvailableFunds() {
      mBinding.useAllFundsCheckbox.setEnabled(false);
      mBinding.useAllFundsCheckbox.setText(R.string.openchannel_max_funds_pleasewait);
      new Thread() {
        @Override
        public void run() {
          try {
            if (getApp() != null) {
              final long feesPerKw = fr.acinq.eclair.package$.MODULE$.feerateByte2Kw(Long.parseLong(mBinding.fundingFeesValue.getText().toString()));
              final long capacitySat = Math.min(getApp().getAvailableFundsAfterFees(feesPerKw).toLong(), Channel.MAX_FUNDING().toLong() - 1);
              runOnUiThread(() -> {
                mBinding.capacityValue.setText(CoinUtils.rawAmountInUnit(new Satoshi(capacitySat), preferredBitcoinUnit).bigDecimal().toPlainString());
                mBinding.capacityValue.setEnabled(false);
              });
            }
          } catch (Throwable t) {
            log.error("could not retrieve max funds from wallet", t);
          } finally {
            runOnUiThread(() -> {
              mBinding.useAllFundsCheckbox.setEnabled(true);
              mBinding.useAllFundsCheckbox.setText(R.string.openchannel_max_funds);
            });
          }
        }
      }.start();
    }
  
    private void runOnUiThread(final Runnable action) {
      if (getActivity() != null) {
        getActivity().runOnUiThread(action);
      }
    }
  
    private App getApp() {
      return (getActivity() != null && getActivity().getApplication() != null) ? (App) getActivity().getApplication() : null;
    }
  
    private void pickFees() {
      if (getApp() != null) {
        if (feeRatingState == Constants.FEE_RATING_SLOW) {
          feeRatingState = Constants.FEE_RATING_MEDIUM;
          mBinding.fundingFeesValue.setText(String.valueOf(getApp().estimateMediumFees()));
          mBinding.setFeeRatingState(feeRatingState);
          mBinding.fundingFeesRating.setText(R.string.payment_fees_medium);
        } else if (feeRatingState == Constants.FEE_RATING_MEDIUM) {
          feeRatingState = Constants.FEE_RATING_FAST;
          mBinding.fundingFeesValue.setText(String.valueOf(getApp().estimateFastFees()));
          mBinding.setFeeRatingState(feeRatingState);
          mBinding.fundingFeesRating.setText(R.string.payment_fees_fast);
        } else if (feeRatingState == Constants.FEE_RATING_FAST) {
          feeRatingState = Constants.FEE_RATING_SLOW;
          mBinding.fundingFeesValue.setText(String.valueOf(getApp().estimateSlowFees()));
          mBinding.setFeeRatingState(feeRatingState);
          mBinding.fundingFeesRating.setText(R.string.payment_fees_slow);
        } else {
          setFeesToDefault();
        }
      }
    }
  
    private void setFeesToDefault() {
      if (getApp() != null) {
        feeRatingState = Constants.FEE_RATING_FAST;
        mBinding.fundingFeesValue.setText(String.valueOf(getApp().estimateFastFees()));
        mBinding.setFeeRatingState(feeRatingState);
        mBinding.fundingFeesRating.setText(R.string.payment_fees_fast);
      }
    }
  
    private void confirmOpenChannel() {
      if (mCallback != null) {
        final Satoshi capacity = extractCapacity(mBinding.capacityValue.getText().toString());
        final Long feesSatPerKW = extractFundingFees(mBinding.fundingFeesValue.getText().toString());
        if (capacity != null && feesSatPerKW != null) {
          // amount and fees are correct, notify parent activity to move to next step
          mCallback.onCapacityConfirm(capacity, feesSatPerKW, mBinding.requestLiquidityCheckbox.isChecked());
        }
      }
    }
  }
  
  