/* This file is part of AHD ID3 Tag Editor (AITE)
   A program can create and edit ID3 Tag for mp3 files

   Copyright © Ala I.Hadid 2012

   This program 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.

   This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;

namespace AHD.ID3Tag
{
    public class ID3v2
    {
        public ID3v2()
        {
            wrapper = new ID3v2QuickWrapper(this);
        }
        byte tagVersion = 3;
        int tagSize = 0;
        //flags
        bool f_Unsynchronisation = false;
        bool f_ExtendedHeader = false;
        bool f_ExperimentalIndicator = false;
        bool f_FooterPresent = false;
        ExtendedHeader eHeader;
        //frames
        List<ID3v2Frame> frames = new List<ID3v2Frame>();
        List<ID3v2Frame> supportedFrames = new List<ID3v2Frame>();
        //others
        string filePath = "";
        ID3v2QuickWrapper wrapper;
        bool dropUnknownFrames = false;
        bool ignoreUncompatibleFrames = false;

        /// <summary>
        /// Get or set the minor version of this ID3 Tag (2.x.0.0 while x=this property)
        /// </summary>
        public byte TagVersion
        { get { return tagVersion; } set { tagVersion = value; } }
        /// <summary>
        /// The size of ID3v2 Tag as located in mp3 EXCLUDING the header 
        /// </summary>
        public int Size
        { get { return tagSize; } }
        /// <summary>
        /// Get the size of current frames, calculated occurding to frames not file.
        /// </summary>
        /// <returns>int Size EXCLUDING the header and not "32 bit synchsafe integer"</returns>
        public int GetCurrentSize()
        {
            int size = 0;
            foreach (ID3v2Frame frame in frames)
            {
                size += frame.Size;
            }
            return size;
        }
        public List<ID3v2Frame> Frames
        { get { return frames; } }
        public List<ID3v2Frame> SupportedFrames
        { get { return supportedFrames; } }
        //Flags
        public bool Flag_Unsynchronisation
        { get { return f_Unsynchronisation; } }
        public bool Flag_ExtendedHeader
        { get { return f_ExtendedHeader; } set { f_ExtendedHeader = value; } }
        public bool Flag_ExperimentalIndicator
        { get { return f_ExperimentalIndicator; } }
        public bool Flag_FooterPresent
        { get { return f_FooterPresent; } set { f_FooterPresent = value; } }
        byte GetFlagsAsByte()
        {
            byte flgs = 0;
            if (f_Unsynchronisation)
                flgs |= 0x80;
            if (f_ExtendedHeader)
                flgs |= 0x40;
            if (f_ExperimentalIndicator)
                flgs |= 0x20;
            if (f_FooterPresent)
                flgs |= 0x10;
            return flgs;
        }
        /// <summary>
        /// Check given file if it has ID3v2 tag
        /// </summary>
        /// <param name="filePath">The complete mp3 file path</param>
        /// <returns>True if it has ID3v2, otherwise false</returns>
        public bool CheckFile(string filePath)
        {
            Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            //read first 3 bytes ID3
            byte[] header = new byte[3];
            stream.Read(header, 0, 3);
            stream.Dispose();
            stream.Close();

            if (header[0] == 0x49 //I
             && header[1] == 0x44 //D
             && header[2] == 0x33)//3
            {
                return true;
            }
            return false;
        }
        /// <summary>
        /// Load ID3v2 tag from mp3 file and fill the Frames list
        /// </summary>
        /// <param name="filePath">The complete mp3 file path</param>
        public void Load(string filePath)
        {
            //detect the supported frames
            DetectedSupportedFrames();
            this.filePath = filePath;
            //check file
            if (!CheckFile(filePath))
                throw new NotID3v2Exception();
            //1 create a read stream
            Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            long index = 0;
            //2 read header
            byte[] header = new byte[10];
            stream.Read(header, 0, 10);
            //we already checked about ID3v2 file identifier in CheckFile method..
            //version
            //byte vMinor = header[4];
            tagVersion = header[3];
            if (tagVersion > 4 || tagVersion < 3)//only 2.3 and 2.4 supported
            {
                stream.Dispose();
                stream.Close();
                throw new NotSupportedID3VersionException("2." + tagVersion.ToString() + ".0.0");
            }

            //flags
            byte flags = header[5];
            f_Unsynchronisation = (flags & 0x80) == 0x80;//bit7
            f_ExtendedHeader = (flags & 0x40) == 0x40;//bit6
            f_ExperimentalIndicator = (flags & 0x20) == 0x20;//bit5
            f_FooterPresent = (flags & 0x10) == 0x10;//bit4
            //size
            tagSize = SynchsafeConvertor.FromSynchsafe(new byte[] { header[6], header[7], header[8], header[9] });
            //2 read Extended Header if found
            if (f_ExtendedHeader)
            {
                eHeader = new ExtendedHeader();
                eHeader.Read(stream);
            }
            index = stream.Position;
            //3 read Frames, start after position, read until end of tag size ...
            for (long i = stream.Position; i < tagSize + index; i++)
            {
                //read frame header
                byte[] frameHeader = new byte[10];
                stream.Read(frameHeader, 0, 10);
                //padding or footer
                if (frameHeader[0] == 0 || frameHeader[0] == 3)
                {
                    break;
                }
                //id
                ASCIIEncoding enc = new ASCIIEncoding();
                string id = enc.GetString(frameHeader, 0, 4);
                //size
                int size = 0;
                if (tagVersion == 3)
                    size = SynchsafeConvertor.FromBytes(new byte[] { frameHeader[4], frameHeader[5], frameHeader[6], frameHeader[7] });
                if (tagVersion == 4)
                    size = SynchsafeConvertor.FromSynchsafe(new byte[] { frameHeader[4], frameHeader[5], frameHeader[6], frameHeader[7] });
                if (size == 0)
                {
                    //currepted frame !?? ignore it
                    continue;
                }
                //flags
                ushort flgs = (ushort)((frameHeader[8] << 8) | frameHeader[9]);
                //data
                byte[] data = new byte[size];
                stream.Read(data, 0, size);

                //determine the frame then add it, unknown frame will be added 
                //if the frame is not supported by this library
                ID3v2Frame frame = GetFrameByID(id);
                frame.ID = id;
                frame.Flags = flgs;
                frame.Data = data;
                frame.ReadSize = size;
                frame.Load();
                frames.Add(frame);

                i = stream.Position;
            }

            //Finish
            stream.Dispose();
            stream.Close();
        }
        /// <summary>
        /// Save ID3v2 to currenlty opened file
        /// </summary>
        public void Save()
        { Save(this.filePath); }
        /// <summary>
        /// Save ID3v2 to a file
        /// </summary>
        /// <param name="filePath">The complete mp3 file path</param>
        public void Save(string filePath)
        {
            this.filePath = filePath;
            //check if there are frames to write
            if (frames.Count == 0)
                throw new Exception("No frame detected to write !!");
            //create dynamic buffer
            List<byte> buffer = new List<byte>();

            //1 write header
            buffer.Add(0x49);//I
            buffer.Add(0x44);//D
            buffer.Add(0x33);//3 
            //write version
            buffer.Add(tagVersion);
            buffer.Add(0);
            //flags as one byte
            buffer.Add(GetFlagsAsByte());
            //calculate size then write it
            int size = GetCurrentSize();
            //we should convert and save that size as "32 bit synchsafe integer"
            buffer.AddRange(SynchsafeConvertor.ToSynchsafeBytes(size));

            //2 if we have Extended Header write it
            if (f_ExtendedHeader)
            {
                eHeader.Save(buffer);
            }

            //3 write frames
            foreach (ID3v2Frame frame in frames)
            {
                //if the user want to discard unknown frames...
                if (dropUnknownFrames)
                    if (frame.GetType() == typeof(UnknownFrame))
                    { continue; }
                //if the user want to discard uncompatible frames
                if (ignoreUncompatibleFrames)
                    if (!frame.IsCompatibleVersion(tagVersion))
                    { continue; }
                //now we can save the frame
                frame.Save();//apply frame data as bytes
                buffer.AddRange(frame.GetHeader(tagVersion));
                buffer.AddRange(frame.Data);
            }

            //4 Footer or padding? ok this library will remove padding even it's
            //exist on read, only footer will be written here if presented
            if (tagVersion == 4)
                if (f_FooterPresent)
                {
                    buffer.Add(0x33);//3 
                    buffer.Add(0x44);//D
                    buffer.Add(0x49);//I
                    //write version
                    buffer.Add(tagVersion);
                    buffer.Add(0);
                    //flags as one byte
                    buffer.Add(GetFlagsAsByte());
                    //size
                    buffer.AddRange(SynchsafeConvertor.ToSynchsafeBytes(size));
                }

            //File streams and other things...
            //create a read stream
            Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            //Clear already saved tag if found
            //read header
            byte[] header = new byte[10];
            stream.Read(header, 0, 10);
            int seek = 0;
            if (header[0] == 0x49 //I
             && header[1] == 0x44 //D
             && header[2] == 0x33)//3
            {
                seek = SynchsafeConvertor.FromSynchsafe(new byte[] { header[6], header[7], header[8], header[9] });
                seek += 10;
            }
            //create temp file
            int i = 0;
            string tempFolder = Path.GetDirectoryName(filePath);
            if (tempFolder.Length == 0)
                tempFolder = Path.GetPathRoot(filePath);

            string tempName = tempFolder + "\\" + Path.GetFileNameWithoutExtension(filePath) + "_temp" + i;
            while (File.Exists(tempName))
            {
                i++;
                tempName = tempFolder + "\\" + Path.GetFileNameWithoutExtension(filePath) + "_temp" + i;
            }
            Stream tempStream = new FileStream(tempName, FileMode.Create, FileAccess.Write);

            //get buffer, the mp3 data and ID3v1 if found
            byte[] tempBuffer = new byte[stream.Length - seek];
            stream.Position = seek;
            stream.Read(tempBuffer, 0, tempBuffer.Length);
            stream.Dispose();
            stream.Close();

            //write the buffer to temp
            tempStream.Write(buffer.ToArray(), 0, buffer.Count);
            tempStream.Write(tempBuffer, 0, tempBuffer.Length);
            tempStream.Dispose();
            tempStream.Close();

            //delete original, rename temp then delete it
            File.Delete(filePath);
            File.Copy(tempName, filePath);
            File.Delete(tempName);
            //done !!
        }
        /// <summary>
        /// Clear ID3v2 from currenlty opened file
        /// </summary>
        public void Clear()
        {
            Clear(this.filePath);
        }
        /// <summary>
        /// Clear ID3v2 from a file
        /// </summary>
        /// <param name="filePath">The complete mp3 file path</param>
        public void Clear(string filePath)
        {
            this.filePath = filePath;
            //check file
            if (!CheckFile(filePath))
                return;
            //all we need to know is the tag size to clear...
            //1 create a read stream
            Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            //2 read header
            byte[] header = new byte[10];
            stream.Read(header, 0, 10);
            //3 get size
            int size = SynchsafeConvertor.FromSynchsafe(new byte[] { header[6], header[7], header[8], header[9] });
            //4 create temp file
            int i = 0;
            string tempFolder = Path.GetDirectoryName(filePath);
            if (tempFolder.Length == 0)
                tempFolder = Path.GetPathRoot(filePath);

            string tempName = tempFolder + "\\" + Path.GetFileNameWithoutExtension(filePath) + "_temp" + i;
            while (File.Exists(tempName))
            {
                i++;
                tempName = tempFolder + "\\" + Path.GetFileNameWithoutExtension(filePath) + "_temp" + i;
            }
            Stream tempStream = new FileStream(tempName, FileMode.Create, FileAccess.Write);
            //5 copy the same file but without ID3 tag

            //get buffer
            byte[] tempBuffer = new byte[stream.Length - (size + 10)];
            stream.Position = size + 10;
            stream.Read(tempBuffer, 0, tempBuffer.Length);
            stream.Dispose();
            stream.Close();

            //write the buffer to temp
            tempStream.Write(tempBuffer, 0, tempBuffer.Length);
            tempStream.Dispose();
            tempStream.Close();

            //6 delete original, rename temp then delete it
            File.Delete(filePath);
            File.Copy(tempName, filePath);
            File.Delete(tempName);
            //done !!
        }

        public ID3v2Frame GetFrameByID(string id)
        {
            foreach (ID3v2Frame frame in supportedFrames)
            {
                if (frame.ID == id)
                {
                    /*
                     * Because of these frames can be added to the tag more than once,
                     * we should create new instance for each one
                     */
                    if (frame.GetType().IsSubclassOf(typeof(UserDefinedTextInformationFrame)))
                        return new UserDefind.TXXX();
                    if (frame.GetType().IsSubclassOf(typeof(UserDefinedURLLinkFrame)))
                        return new UserDefind.WXXX();
                    if (frame.GetType().IsSubclassOf(typeof(UnsynchronisedLyricsFrame)))
                        return new UnsynchronisedLyricsFrames.USLT();
                    if (frame.GetType().IsSubclassOf(typeof(SynchronisedLyricsFrame)))
                        return new SynchronisedLyricsFrames.SYLT();
                    if (frame.GetType().IsSubclassOf(typeof(CommentsFrame)))
                        return new CommentsFrames.COMM();
                    if (frame.GetType().IsSubclassOf(typeof(AttachedPictureFrame)))
                        return new AttachedPictureFrames.APIC();
                    if (frame.GetType().IsSubclassOf(typeof(PopularimeterFrame)))
                        return new PopularimeterFrames.POPM();
                    if (frame.GetType().IsSubclassOf(typeof(GeneralEncapsulatedObjectFrame)))
                        return new GeneralEncapsulatedObjectFrames.GEOB();
                    else
                        /*Normal frame, should be added once in the tag*/
                        return frame;
                }
            }
            return new UnknownFrame(id, 0, 0, null);
        }
        public ID3v2Frame GetFrameLoaded(string id)
        {
            foreach (ID3v2Frame frame in frames)
            {
                if (frame.ID == id)
                {
                    return frame;
                }
            }
            return null;
        }
        public ID3v2Frame[] GetFramesLoaded(string id)
        {
            List<ID3v2Frame> fframes = new List<ID3v2Frame>();
            foreach (ID3v2Frame frame in frames)
            {
                if (frame.ID == id)
                {
                    fframes.Add(frame);
                }
            }
            return fframes.ToArray();
        }
        /// <summary>
        /// Remove all frames of the given ID, the frames that can be added more than once (like COMM) will be removed
        ///  from the tag as it found
        /// </summary>
        /// <param name="id">The id of the frame to remove</param>
        public void RemoveFrame(string id)
        {
            for (int i = 0; i < frames.Count; i++)
            {
                if (frames[i].ID == id)
                {
                    frames.RemoveAt(i);
                    i = -1;
                }
            }
        }
        /// <summary>
        /// Remove frame of the given ID, the frames that can be added more than once (like COMM) the first found of it will be removed
        /// </summary>
        /// <param name="id">The id of the frame to remove</param>
        public void RemoveFrameOnce(string id)
        {
            for (int i = 0; i < frames.Count; i++)
            {
                if (frames[i].ID == id)
                {
                    frames.RemoveAt(i);
                    break;
                }
            }
        }
        public void DetectedSupportedFrames()
        {
            supportedFrames.Clear();
            Type[] types = Assembly.GetExecutingAssembly().GetTypes();
            foreach (Type tp in types)
            {
                if (tp.IsSubclassOf(typeof(TextFrame)))
                {
                    TextFrame frame = Activator.CreateInstance(tp) as TextFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(URLFrame)))
                {
                    URLFrame frame = Activator.CreateInstance(tp) as URLFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(UserDefinedTextInformationFrame)))
                {
                    UserDefinedTextInformationFrame frame = Activator.CreateInstance(tp) as UserDefinedTextInformationFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(UserDefinedURLLinkFrame)))
                {
                    UserDefinedURLLinkFrame frame = Activator.CreateInstance(tp) as UserDefinedURLLinkFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(EventTimingCodesFrame)))
                {
                    EventTimingCodesFrame frame = Activator.CreateInstance(tp) as EventTimingCodesFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(UnsynchronisedLyricsFrame)))
                {
                    UnsynchronisedLyricsFrame frame = Activator.CreateInstance(tp) as UnsynchronisedLyricsFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(SynchronisedLyricsFrame)))
                {
                    SynchronisedLyricsFrame frame = Activator.CreateInstance(tp) as SynchronisedLyricsFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(CommentsFrame)))
                {
                    CommentsFrame frame = Activator.CreateInstance(tp) as CommentsFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(AttachedPictureFrame)))
                {
                    AttachedPictureFrame frame = Activator.CreateInstance(tp) as AttachedPictureFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(PlayCounterFrame)))
                {
                    PlayCounterFrame frame = Activator.CreateInstance(tp) as PlayCounterFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(PopularimeterFrame)))
                {
                    PopularimeterFrame frame = Activator.CreateInstance(tp) as PopularimeterFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(InvolvedPeopleListFrame)))
                {
                    InvolvedPeopleListFrame frame = Activator.CreateInstance(tp) as InvolvedPeopleListFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(GeneralEncapsulatedObjectFrame)))
                {
                    GeneralEncapsulatedObjectFrame frame = Activator.CreateInstance(tp) as GeneralEncapsulatedObjectFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(TermsOfUseFrame)))
                {
                    TermsOfUseFrame frame = Activator.CreateInstance(tp) as TermsOfUseFrame;
                    supportedFrames.Add(frame);
                }
                if (tp.IsSubclassOf(typeof(CommercialFrame)))
                {
                    CommercialFrame frame = Activator.CreateInstance(tp) as CommercialFrame;
                    supportedFrames.Add(frame);
                }
            }
        }
        public string FilePath
        { get { return filePath; } set { filePath = value; } }
        /// <summary>
        /// Get or set a value indecate wether to drop unknown frames on save
        /// </summary>
        public bool DropUnknownFrames
        { get { return dropUnknownFrames; } set { dropUnknownFrames = value; } }
        /// <summary>
        /// Get a value indecate wether to ignore uncompatible frames with current tag version
        /// </summary>
        public bool IgnoreUncompatibleFrames
        { get { return ignoreUncompatibleFrames; } set { ignoreUncompatibleFrames = value; } }
        /// <summary>
        /// Get the class that used to get the most common frames without the need to deal with "Frames" property
        /// </summary>
        public ID3v2QuickWrapper QuickWrapper
        { get { return wrapper; } }
    }
}
