/*
 * Copyright 2002-2019 Drew Noakes and contributors
 *
 *    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.
 *
 * More information about this project is available at:
 *
 *    https://drewnoakes.com/code/exif/
 *    https://github.com/drewnoakes/metadata-extractor
 */
package com.drew.metadata.mp4;

import com.drew.imaging.mp4.Mp4Handler;
import com.drew.lang.DateUtil;
import com.drew.lang.Rational;
import com.drew.lang.SequentialByteArrayReader;
import com.drew.lang.SequentialReader;
import com.drew.lang.annotations.NotNull;
import com.drew.lang.annotations.Nullable;
import com.drew.metadata.Metadata;
import com.drew.metadata.mp4.media.*;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.drew.metadata.mp4.Mp4Directory.TAG_LATITUDE;
import static com.drew.metadata.mp4.Mp4Directory.TAG_LONGITUDE;

/**
 * @author Payton Garland
 */
public class Mp4BoxHandler extends Mp4Handler<Mp4Directory>
{
    public Mp4BoxHandler(Metadata metadata)
    {
        super(metadata);
    }

    @NotNull
    @Override
    protected Mp4Directory getDirectory()
    {
        return new Mp4Directory();
    }

    @Override
    public boolean shouldAcceptBox(@NotNull String type)
    {
        return type.equals(Mp4BoxTypes.BOX_FILE_TYPE)
            || type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER)
            || type.equals(Mp4BoxTypes.BOX_HANDLER)
            || type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER)
            || type.equals(Mp4BoxTypes.BOX_TRACK_HEADER)
            || type.equals(Mp4BoxTypes.BOX_USER_DATA)
            || type.equals(Mp4BoxTypes.BOX_USER_DEFINED);
    }

    @Override
    public boolean shouldAcceptContainer(@NotNull String type)
    {
        return type.equals(Mp4ContainerTypes.BOX_TRACK)
            || type.equals(Mp4ContainerTypes.BOX_METADATA)
            || type.equals(Mp4ContainerTypes.BOX_MOVIE)
            || type.equals(Mp4ContainerTypes.BOX_MEDIA);
    }

    @Override
    public Mp4Handler<?> processBox(@NotNull String type, @Nullable byte[] payload, long boxSize, Mp4Context context) throws IOException
    {
        if (payload != null) {
            SequentialReader reader = new SequentialByteArrayReader(payload);
            if (type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER)) {
                processMovieHeader(reader);
            } else if (type.equals(Mp4BoxTypes.BOX_FILE_TYPE)) {
                processFileType(reader, boxSize);
            } else if (type.equals(Mp4BoxTypes.BOX_HANDLER)) {

                // ISO/IED 14496-12:2015 pg.7

                reader.skip(4); // one byte version, three bytes flags

                // ISO/IED 14496-12:2015 pg.30

                reader.skip(4); // Pre-defined
                String handlerType = reader.getString(4);
                reader.skip(12); // Reserved
                String name = reader.getNullTerminatedString((int)boxSize - 32, Charset.defaultCharset());

                final String HANDLER_SOUND_MEDIA             = "soun";
                final String HANDLER_VIDEO_MEDIA             = "vide";
                final String HANDLER_HINT_MEDIA              = "hint";
                final String HANDLER_TEXT_MEDIA              = "text";
                final String HANDLER_META_MEDIA              = "meta";

                if (handlerType.equals(HANDLER_SOUND_MEDIA)) {
                    return new Mp4SoundHandler(metadata, context);
                } else if (handlerType.equals(HANDLER_VIDEO_MEDIA)) {
                    return new Mp4VideoHandler(metadata, context);
                } else if (handlerType.equals(HANDLER_HINT_MEDIA)) {
                    return new Mp4HintHandler(metadata, context);
                } else if (handlerType.equals(HANDLER_TEXT_MEDIA)) {
                    return new Mp4TextHandler(metadata, context);
                } else if (handlerType.equals(HANDLER_META_MEDIA)) {
                    return new Mp4MetaHandler(metadata, context);
                }
                return this;
            } else if (type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER)) {
                processMediaHeader(reader, context);
            } else if (type.equals(Mp4BoxTypes.BOX_TRACK_HEADER)) {
                processTrackHeader(reader);
            } else if (type.equals(Mp4BoxTypes.BOX_USER_DEFINED)) {
                Mp4UuidBoxHandler userBoxHandler = new Mp4UuidBoxHandler(metadata);
                userBoxHandler.processBox(type, payload, boxSize, context);
            } else if (type.equals(Mp4BoxTypes.BOX_USER_DATA)) {
                processUserData(reader, payload.length);
            }
        } else {
            if (type.equals(Mp4ContainerTypes.BOX_COMPRESSED_MOVIE)) {
                directory.addError("Compressed MP4 movies not supported");
            }
        }
        return this;
    }

    private static final Pattern COORDINATE_PATTERN = Pattern.compile("([+-]\\d+\\.\\d+)([+-]\\d+\\.\\d+)");

    private void processUserData(@NotNull SequentialReader reader, int length) throws IOException
    {
        final int LOCATION_CODE = 0xA978797A; // "©xyz"

        String coordinateString = null;

        while (reader.getPosition() < length) {
            long size = reader.getUInt32();
            if (size <= 4)
                break;
            int kind = reader.getInt32();
            if (kind == LOCATION_CODE) {
                int xyzLength = reader.getUInt16();
                reader.skip(2);
                coordinateString = reader.getString(xyzLength, "UTF-8");
            } else if (size >= 8) {
                reader.skip(size - 8);
            } else {
                return;
            }
        }

        if (coordinateString != null) {
            final Matcher matcher = COORDINATE_PATTERN.matcher(coordinateString);
            if (matcher.find()) {
                final double latitude = Double.parseDouble(matcher.group(1));
                final double longitude = Double.parseDouble(matcher.group(2));

                directory.setDouble(TAG_LATITUDE, latitude);
                directory.setDouble(TAG_LONGITUDE, longitude);
            }
        }
    }

    private void processFileType(@NotNull SequentialReader reader, long boxSize) throws IOException
    {
        // ISO/IED 14496-12:2015 pg.8

        String majorBrand = reader.getString(4);
        long minorVersion = reader.getUInt32();

        // TODO avoid array list
        ArrayList<String> compatibleBrands = new ArrayList<String>();
        for (int i = 16; i < boxSize; i += 4) {
            compatibleBrands.add(reader.getString(4));
        }

        directory.setString(Mp4Directory.TAG_MAJOR_BRAND, majorBrand);
        directory.setLong(Mp4Directory.TAG_MINOR_VERSION, minorVersion);
        directory.setStringArray(Mp4Directory.TAG_COMPATIBLE_BRANDS, compatibleBrands.toArray(new String[compatibleBrands.size()]));
    }

    private void processMovieHeader(@NotNull SequentialReader reader) throws IOException
    {
        // ISO/IED 14496-12:2015 pg.23

        short version = reader.getUInt8();

        reader.skip(3); // flags

        long creationTime;
        long modificationTime;
        long timescale;
        long duration;

        if (version == 1) {
            creationTime = reader.getInt64();
            modificationTime = reader.getInt64();
            timescale = reader.getUInt32();
            duration = reader.getInt64();
        } else {
            creationTime = reader.getUInt32();
            modificationTime = reader.getUInt32();
            timescale = reader.getUInt32();
            duration = reader.getUInt32();
        }

        int rate = reader.getInt32();
        int volume = reader.getInt16();
        reader.skip(2); // Reserved
        reader.skip(8); // Reserved
        int[] matrix = new int[]{
            reader.getInt32(),
            reader.getInt32(),
            reader.getInt32(),
            reader.getInt32(),
            reader.getInt32(),
            reader.getInt32(),
            reader.getInt32(),
            reader.getInt32(),
            reader.getInt32()
        };
        reader.skip(24); // Pre-defined
        long nextTrackID = reader.getUInt32();

        // Get creation/modification times
        directory.setDate(Mp4Directory.TAG_CREATION_TIME, DateUtil.get1Jan1904EpochDate(creationTime));
        directory.setDate(Mp4Directory.TAG_MODIFICATION_TIME, DateUtil.get1Jan1904EpochDate(modificationTime));

        // Get duration and time scale
        directory.setLong(Mp4Directory.TAG_DURATION, duration);
        directory.setLong(Mp4Directory.TAG_TIME_SCALE, timescale);
        directory.setRational(Mp4Directory.TAG_DURATION_SECONDS, new Rational(duration, timescale));

        directory.setIntArray(Mp4Directory.TAG_TRANSFORMATION_MATRIX, matrix);

        // Calculate preferred rate fixed point
        double preferredRateInteger = (rate & 0xFFFF0000) >> 16;
        double preferredRateFraction = (rate & 0x0000FFFF) / Math.pow(2, 4);
        directory.setDouble(Mp4Directory.TAG_PREFERRED_RATE, preferredRateInteger + preferredRateFraction);

        // Calculate preferred volume fixed point
        double preferredVolumeInteger = (volume & 0xFF00) >> 8;
        double preferredVolumeFraction = (volume & 0x00FF) / Math.pow(2, 2);
        directory.setDouble(Mp4Directory.TAG_PREFERRED_VOLUME, preferredVolumeInteger + preferredVolumeFraction);

        directory.setLong(Mp4Directory.TAG_NEXT_TRACK_ID, nextTrackID);
    }

    private void processMediaHeader(@NotNull SequentialReader reader, Mp4Context context) throws IOException
    {
        // ISO/IED 14496-12:2015 pg.7

        int version = reader.getUInt8();

        reader.skip(3); // flags

        // ISO/IED 14496-12:2015 pg.29

        if (version == 1) {
            context.creationTime = reader.getInt64();
            context.modificationTime = reader.getInt64();
            context.timeScale = (long)reader.getInt32();
            context.duration = reader.getInt64();
        } else {
            context.creationTime = reader.getUInt32();
            context.modificationTime = reader.getUInt32();
            context.timeScale = reader.getUInt32();
            context.duration = reader.getUInt32();
        }

        int languageBits = reader.getInt16();

        context.language = new String(new char[]
            {
                (char)(((languageBits & 0x7C00) >> 10) + 0x60),
                (char)(((languageBits & 0x03E0) >> 5) + 0x60),
                (char)((languageBits & 0x001F) + 0x60)
            });
    }

    private void processTrackHeader(@NotNull SequentialReader reader) throws IOException
    {
        // ISO/IED 14496-12:2015 pg.7

        int version = reader.getUInt8();

        reader.skip(3); // flags

        // ISO/IED 14496-12:2005 pg.17-18

        long creationTime;
        long modificationTime;
        long trackID;
        long duration;

        if (version == 1) {
            creationTime = reader.getInt64();
            modificationTime = reader.getInt64();
            trackID = reader.getInt32();
            reader.skip(4); // reserved
            duration = reader.getInt64();
        } else {
            creationTime = reader.getUInt32();
            modificationTime = reader.getUInt32();
            trackID = reader.getUInt32();
            reader.skip(4);
            duration = reader.getUInt32();
        }

        reader.skip(8); // reserved

        int layer = reader.getInt16();
        int alternateGroup = reader.getInt16();
        int volume = reader.getInt16();

        reader.skip(2); // reserved

        int[] matrix = new int[9];
        for (int i = 0; i < 9; i++) {
            matrix[i] = reader.getInt32();
        }

        long width = reader.getInt32();
        long height = reader.getInt32();

        // TODO seems wrong to only set this once
        if (width != 0 && height != 0 && directory.getDoubleObject(Mp4Directory.TAG_ROTATION) == null) {
            int x = matrix[1] + matrix[4];
            int y = matrix[0] + matrix[3];
            double theta = Math.atan2(y, x);
            double degree = Math.toDegrees(theta);
            degree -= 45;
            directory.setDouble(Mp4Directory.TAG_ROTATION, degree);
        }
    }
}
