package school_project.Parsers;

import school_project.Map;
import school_project.Piece;
import school_project.Utils.Bitwise;
import school_project.Vec2;

import java.io.*;
import java.util.Arrays;

public class BinaryParser implements FileParser {

    @Override
    public Map getLevel(File file, boolean saved_data) throws IOException {
        Map ret;

        FileInputStream fileStream = new FileInputStream(file);

        byte[] level_data = ExtractLevelData(fileStream);

        ret = new Map(ExtractMapFromLevelData(level_data));

        ret.addPiece(ExtractPiecesFromLevelData(level_data, saved_data));
        
        fileStream.close();
        return ret;
    }

    @Override
    public void saveLevel(File file, Map level_data, boolean save_data) throws IOException {
        int byteSize = getByteSizeForMap(level_data, save_data);
        byte[] data = new byte[byteSize];
        int i = 0;
        data[i++] = 'S'; data[i++] = 'M'; data[i++] = 'S';
        data[i++] = (byte) level_data.getWidth(); data[i++] = (byte) level_data.getHeight();
        for(byte b : BuildByteFromMatrix(level_data.getShape())){
            data[i++] = b;
        }
        data[i++] = (byte) level_data.getPieces().size();
        for (Piece p : level_data.getPieces()) {
            data[i++] = Bitwise.NibbleToByte((byte) p.getWidth(), (byte) p.getHeight());
            for(byte b : BuildByteFromMatrix(p.getShape())){
                data[i++] = b;
            }
        }
        if (save_data){
            for (Piece p : level_data.getPieces()) {
                Vec2 _piece_pos = p.getPosition();
                if(_piece_pos == null){
                    data[i++] = 'F';
                    data[i++] = 'L';
                }else{
                    data[i++] = (byte) _piece_pos.x;
                    data[i++] = (byte) _piece_pos.y;
                }
            }
        }
        data[i++] = 'S'; data[i++] = 'M'; data[i++] = 'E';
        FileOutputStream save_file = new FileOutputStream(file);
        save_file.write(data);
        save_file.flush();
        save_file.close();
    }

    /**
     * Extract Level data from file content
     * @param fileStream file stream to read extract data from
     * @return Level data as an array of byte
     * @throws IOException Expected if we can't read the file
     */
    static byte[] ExtractLevelData(InputStream fileStream) throws IOException {

        byte[] bytes = fileStream.readAllBytes();

        int start_position = 0, end_position = 0;
        for (int i = 0; i < bytes.length; i++) {
            if(bytes[i] == 83 && bytes[i+1] == 77 && bytes[i+2] == 83){ // SMS
                start_position = i+3;
                break;
            }
        }

        for (int i = start_position; i < bytes.length - 2; i++) {
            if(bytes[i] == 83 && bytes[i+1] == 77 && bytes[i+2] == 69){ // SME
                end_position = i;
                break;
            }
        }

        return Arrays.copyOfRange(bytes, start_position, end_position);
    }

    /**
     * Get Pieces out of the level data
     *
     * @param levelData  full data of the level without header and footer
     * @param saved_data Should extract saved data and included it in the pieces
     * @return array of Piece from level data
     */
    static Piece[] ExtractPiecesFromLevelData(byte[] levelData, boolean saved_data) {
        byte map_width = levelData[0], map_height = levelData[1];
        byte piece_count = levelData[2 + map_width * map_height / 8 + (map_height * map_width % 8 != 0 ? 1 : 0)];
        Piece[] ret = new Piece[piece_count];
        byte[] pieces_data = Arrays.copyOfRange(levelData, 3 + map_width * map_height / 8 + (map_height * map_width % 8 != 0 ? 1 : 0), levelData.length);
        byte[] pieces_positions = saved_data ? Arrays.copyOfRange(levelData, levelData.length - piece_count*2,levelData.length ): null;
        int piece_offset = 0;
        for (int piece_index = 0; piece_index < piece_count; piece_index++) {
            Vec2 _piece_size = Bitwise.ByteToNible(pieces_data[piece_index + piece_offset]);

            byte[] _piece_data = Arrays.copyOfRange(pieces_data, piece_index + piece_offset + 1, piece_index + piece_offset + 1 + _piece_size.x * _piece_size.y / 8 + (_piece_size.x * _piece_size.y % 8 != 0 ? 1 : 0));

            boolean[][] _piece_matrix = BuildMatrixFromBytes(_piece_size.x, _piece_size.y, _piece_data);

            ret[piece_index] = new Piece(_piece_matrix);

            if(saved_data){
                Vec2 _piece_pos = new Vec2(pieces_positions[piece_index*2], pieces_positions[piece_index*2 + 1]);
                ret[piece_index].setPosition(_piece_pos);
            }

            piece_offset += _piece_size.x * _piece_size.y / 8 + (_piece_size.x * _piece_size.y % 8 != 0 ? 1 : 0);
        }
        return ret;
    }

    /**
     * Get the Map Matrix out of the level data
     * @param level_data full data of the level without header and footer
     * @return boolean matrix of the map
     */
    static boolean[][] ExtractMapFromLevelData(byte[] level_data){
        int map_width = level_data[0], map_height = level_data[1];
        byte[] map_data = Arrays.copyOfRange(level_data, 2, 2 + map_width * map_height / 8 + (map_height * map_width % 8 != 0 ? 1 : 0));
        return BuildMatrixFromBytes(map_width, map_height, map_data);
    }

    /**
     * take a boolean matrix and build an array of byte following the specs of the parser
     * @param shape bolean matrix where true are 1 and false are 0
     * @return byte array with each element compiled for file format
     */
    static byte[] BuildByteFromMatrix(boolean[][] shape){
        int width = shape[0].length , height = shape.length;
        boolean[] b_list = new boolean[width * height];
        for (int x = 0; x < shape.length; x++) {
            for (int y = 0; y < shape[x].length; y++) {
                b_list[x * shape[x].length + y] = shape[x][y];
            }
        }
        byte[] ret = new byte[width * height  / 8 + (width * height % 8 == 0 ? 0 : 1)];
        for (int i = 0; i < ret.length; i++) {
            byte current_byte = 0;
            boolean[] current_byte_data = Arrays.copyOfRange(b_list, i * 8, i * 8 + 8);
            for (boolean curr_data: current_byte_data) {
                current_byte = (byte) (current_byte << 1);
                current_byte = (byte) (current_byte | (curr_data ? 1 : 0));
            }
            ret[i] = current_byte;
        }
        return ret;
    }

    /**
     * Build a boolean Matrix From a byte array
     * Each Byte is composed of 8 bit, each bit is 1 or 0
     *  if the bit is 0 then it's a false for this cell
     *  else it's true for this cell
     * @param matrix_width width of the matrix
     * @param matrix_height height of the matrix
     * @param matrix_data byte array of the data to export
     * @return boolean Matrix of the data decompiled
     */
    static boolean[][] BuildMatrixFromBytes(int matrix_width, int matrix_height, byte[] matrix_data){
        boolean[][] ret = new boolean[matrix_height][matrix_width];

        // Transforming the bit from matrix_data's byte into boolean array for better manipulation
        boolean[] b_array = new boolean[matrix_height * matrix_width];
        int index = 0;
        for(byte b: matrix_data){
            for (int i = 0; i < 8; i++) { // because 8 bit in a byte
                b_array[index] = Bitwise.IsBitSetAt(b, i);
                index++;
                if(index >= matrix_height * matrix_width)
                    break;
            }
        }

        // Transforming b_array to a 2D matrix
        for (int x = 0; x < matrix_height; x++) {
            for (int y = 0; y < matrix_width; y++) {
                ret[x][y] = b_array[y + x * matrix_width];
            }
        }
        return ret;
    }

    /**
     * give the amount of byte needed to store the given Map
     * following the binary file format
     * @param level the map to check
     * @param data should add save data or only level data
     * @return integer of the ammount of byte needed
     */
    public static int getByteSizeForMap(Map level, boolean data){
        int ret = 6; // header + footer
        ret += 2; //size of the piece
        ret += ((level.getWidth() * level.getHeight()) / 8); // size of the map
        ret += level.getHeight() * level.getWidth() % 8 == 0 ? 0 : 1; // Add 1 if the size of map is not a mult of 8
        ret += 1; // amount of pieces
        for(Piece p: level.getPieces()){
            ret += 1; // size of the piece
            ret += p.getHeight() * p.getWidth() / 8;
            ret += p.getHeight() * p.getWidth() % 8 == 0 ? 0 : 1; // add 1 if the size of the piece is not mult of 8
            if(data){
                ret += 2; // if the piece is not placed, only one byte else 2
            }
        }
        return ret;
    }
}
