/*
 * Copyright 2018 Daniel Underhay & Matthew Daley.
 *
 * This file is part of Walrus.
 *
 * Walrus is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Walrus is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Walrus.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.bugfuzz.android.projectwalrus.card.carddata;

import android.support.annotation.Nullable;
import android.support.annotation.Size;

import com.bugfuzz.android.projectwalrus.R;
import com.bugfuzz.android.projectwalrus.util.MiscUtils;
import com.google.common.io.BaseEncoding;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

// TODO XXX: check all checks made on all constructors here
@CardData.Metadata(
        name = "MIFARE",
        iconId = R.drawable.drawable_mifare
)
public class MifareCardData extends ISO14443ACardData {

    public final List<HistoricalReadStep> readStepHistory;
    private final Map<Integer, Block> blocks;

    public MifareCardData() {
        blocks = new HashMap<>();
        readStepHistory = new ArrayList<>();
    }

    public MifareCardData(MifareCardData other) {
        this(other, other.blocks);
    }

    public MifareCardData(ISO14443ACardData iso14443APart, @Nullable Map<Integer, Block> blocks) {
        super(iso14443APart);

        this.blocks = blocks != null ? blocks : new HashMap<Integer, Block>();

        readStepHistory = new ArrayList<>();
    }

    @SuppressWarnings("unused")
    public static MifareCardData newDebugInstance() {
        return new MifareCardData(new ISO14443ACardData((short) 0x0004,
                new BigInteger(32, new Random()), (byte) 0x08, new byte[]{}), null);
    }

    public void setBlock(int blockNumber, Block block) {
        if (blockNumber < 0 || blockNumber > 255) {
            throw new RuntimeException("Invalid block number");
        }

        blocks.put(blockNumber, block);
    }

    public Map<Integer, Block> getBlocks() {
        return Collections.unmodifiableMap(blocks);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        MifareCardData that = (MifareCardData) o;

        return new EqualsBuilder()
                .appendSuper(super.equals(o))
                .append(blocks, that.blocks)
                .append(readStepHistory, that.readStepHistory)
                .isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37)
                .appendSuper(super.hashCode())
                .append(blocks)
                .append(readStepHistory)
                .toHashCode();
    }

    public enum KeySlot {
        A,
        B
    }

    public static class Block implements Serializable {

        public static final int SIZE = 16;

        public final byte[] data;

        public Block(@Size(SIZE) byte[] data) {
            if (data.length != SIZE) {
                throw new IllegalArgumentException("Invalid data length");
            }

            this.data = data;
        }

        @Override
        public String toString() {
            return MiscUtils.bytesToHex(data, false);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }

            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            Block block = (Block) o;

            return new EqualsBuilder()
                    .append(data, block.data)
                    .isEquals();
        }

        @Override
        public int hashCode() {
            return new HashCodeBuilder(17, 37)
                    .append(data)
                    .toHashCode();
        }
    }

    public static class Key implements Serializable {

        public final byte[] key;

        public Key(@Size(6) byte[] key) {
            if (key.length != 6) {
                throw new IllegalArgumentException("Invalid key length");
            }

            this.key = key;
        }

        public static Key fromString(String value) {
            return new Key(BaseEncoding.base16().decode(value.toUpperCase()));
        }

        @Override
        public String toString() {
            return MiscUtils.bytesToHex(key, false);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }

            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            Key key1 = (Key) o;

            return new EqualsBuilder()
                    .append(key, key1.key)
                    .isEquals();
        }

        @Override
        public int hashCode() {
            return new HashCodeBuilder(17, 37)
                    .append(key)
                    .toHashCode();
        }
    }

    public static class HistoricalReadStep implements Serializable {

        public final int blockNumber;
        public final Key key;
        public final KeySlot keySlot;
        public final boolean success;

        public HistoricalReadStep(int blockNumber, Key key, KeySlot keySlot, boolean success) {
            this.blockNumber = blockNumber;
            this.key = key;
            this.keySlot = keySlot;
            this.success = success;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }

            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            HistoricalReadStep that = (HistoricalReadStep) o;

            return new EqualsBuilder()
                    .append(success, that.success)
                    .append(blockNumber, that.blockNumber)
                    .append(key, that.key)
                    .append(keySlot, that.keySlot)
                    .isEquals();
        }

        @Override
        public int hashCode() {
            return new HashCodeBuilder(17, 37)
                    .append(blockNumber)
                    .append(key)
                    .append(keySlot)
                    .append(success)
                    .toHashCode();
        }
    }
}