/**
 * CUETools.Flake: pure managed FLAC audio encoder
 * Copyright (c) 2009-2021 Grigory Chudov
 * Based on Flake encoder, http://flake-enc.sourceforge.net/
 * Copyright (c) 2006-2009 Justin Ruggles
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#define NOINTEROP
#define VARIANT1

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using CUETools.Codecs.Flake.Properties;
#if INTEROP
using System.Runtime.InteropServices;
#endif

namespace CUETools.Codecs.Flake;

public class AudioEncoder : IAudioDest
{
    Stream _IO;

    // number of audio channels
    // set by user prior to calling flake_encode_init
    // valid values are 1 to 8
    readonly int channels;
    int          ch_code;

    // audio sample rate in Hz
    // set by user prior to calling flake_encode_init
    int sr_code0, sr_code1;

    // sample size in bits
    // set by user prior to calling flake_encode_init
    // only 16-bit is currently supported
    int bps_code;

    // total stream samples
    // set by user prior to calling flake_encode_init
    // if 0, stream length is unknown
    int sample_count = -1;

    FlakeEncodeParams eparams;

    // maximum frame size in bytes
    // set by flake_encode_init
    // this can be used to allocate memory for output
    int max_frame_size;

    byte[] frame_buffer;

    int frame_count;

    long first_frame_offset;

#if INTEROP
		TimeSpan _userProcessorTime;
#endif

    // header bytes
    // allocated by flake_encode_init and freed by flake_encode_close
    byte[] header;

    readonly int[]                samplesBuffer;
    int[]                         verifyBuffer;
    readonly int[]                residualBuffer;
    readonly float[]              windowBuffer;
    readonly double[]             windowScale;
    readonly LpcWindowSection[,,] windowSections;

    readonly WindowFunction[] windowType;
    int                       samplesInBuffer;

    int m_blockSize;
    int _windowsize, _windowcount;

    readonly Crc8 crc8;
    MD5           md5;

    FlacFrame    frame;
    AudioDecoder verify;

    SeekPoint[] seek_table;
    int         seek_table_offset = -1;

    bool inited;

    public AudioEncoder(EncoderSettings settings, string path, Stream IO = null)
    {
        m_settings = settings.Clone() as EncoderSettings;
        m_settings.Validate();

        //if (Settings.PCM.BitsPerSample != 16)
        //    throw new Exception("Bits per sample must be 16.");
        //if (Settings.PCM.ChannelCount != 2)
        //    throw new Exception("ChannelCount must be 2.");

        channels = Settings.PCM.ChannelCount;

        // flake_validate_params

        Path = path;
        _IO  = IO;

        samplesBuffer  = new int[FlakeConstants.MAX_BLOCKSIZE   * (channels == 2 ? 4 : channels)];
        residualBuffer = new int[FlakeConstants.MAX_BLOCKSIZE   * (channels == 2 ? 10 : channels + 1)];
        windowBuffer   = new float[FlakeConstants.MAX_BLOCKSIZE * 2 * lpc.MAX_LPC_WINDOWS];
        windowScale    = new double[lpc.MAX_LPC_WINDOWS];
        windowType     = new WindowFunction[lpc.MAX_LPC_WINDOWS];
        windowSections = new LpcWindowSection[12, lpc.MAX_LPC_WINDOWS, lpc.MAX_LPC_SECTIONS];

        eparams.flake_set_defaults(m_settings);

        crc8  = new Crc8();
        frame = new FlacFrame(channels * 2);
    }

    public int TotalSize { get; private set; }

    readonly EncoderSettings m_settings;

    public IAudioEncoderSettings Settings => m_settings;

#if INTEROP
		[DllImport("kernel32.dll")]
		static extern bool GetThreadTimes(IntPtr hThread, out long lpCreationTime, out long lpExitTime, out long lpKernelTime, out long lpUserTime);
		[DllImport("kernel32.dll")]
		static extern IntPtr GetCurrentThread();
#endif

    void DoClose()
    {
        if(inited)
        {
            while(samplesInBuffer > 0)
            {
                m_blockSize = samplesInBuffer;
                output_frame();
            }

            if(_IO.CanSeek)
            {
                if(sample_count <= 0 && Position != 0)
                {
                    var bitwriter = new BitWriter(header, 0, 4);
                    bitwriter.writebits(32, (int)Position);
                    bitwriter.flush();
                    _IO.Position = 22;
                    _IO.Write(header, 0, 4);
                }

                if(md5 != null)
                {
                    md5.TransformFinalBlock(frame_buffer, 0, 0);
                    _IO.Position = 26;
                    _IO.Write(md5.Hash, 0, md5.Hash.Length);
                }

                if(seek_table != null)
                {
                    _IO.Position = seek_table_offset;
                    int len = write_seekpoints(header, 0, 0);
                    _IO.Write(header, 4, len - 4);
                }
            }

            _IO.Close();
            inited = false;
        }

#if INTEROP
			long fake, KernelStart, UserStart;
			GetThreadTimes(GetCurrentThread(), out fake, out fake, out KernelStart, out UserStart);
			_userProcessorTime = new TimeSpan(UserStart);
#endif
    }

    public void Close()
    {
        DoClose();

        if(sample_count > 0 && Position != sample_count) throw new Exception(Resources.ExceptionSampleCount);
    }

    public void Delete()
    {
        if(inited)
        {
            _IO.Close();
            inited = false;
        }

        if(Path != "") File.Delete(Path);
    }

    public long Position { get; private set; }

    public long FinalSampleCount
    {
        set => sample_count = (int)value;
    }

    public OrderMethod OrderMethod
    {
        get => eparams.order_method;
        set => eparams.order_method = value;
    }

    public int DevelopmentMode
    {
        get => eparams.development_mode;
        set => eparams.development_mode = value;
    }

    public bool DoSeekTable
    {
        get => eparams.do_seektable;
        set => eparams.do_seektable = value;
    }

    public int VBRMode
    {
        get => eparams.variable_block_size;
        set => eparams.variable_block_size = value;
    }

    public TimeSpan UserProcessorTime
    {
        get
        {
#if INTEROP
				return _userProcessorTime;
#else
            return new TimeSpan(0);
#endif
        }
    }

    unsafe int get_wasted_bits(int* signal, int samples)
    {
        int i, shift;
        var x = 0;

        for(i = 0; i < samples && 0 == (x & 1); i++) x |= signal[i];

        if(x == 0)
            shift = 0;
        else
            for(shift = 0; 0 == (x & 1); shift++)
                x >>= 1;

        if(shift > 0)
            for(i = 0; i < samples; i++)
                signal[i] >>= shift;

        return shift;
    }

    /// <summary>
    ///     Copy channel-interleaved input samples into separate subframes
    /// </summary>
    /// <param name="samples"></param>
    /// <param name="pos"></param>
    /// <param name="block"></param>
    unsafe void copy_samples(int[,] samples, int pos, int block)
    {
        fixed(int* fsamples = samplesBuffer, src = &samples[pos, 0])
        {
            if(channels == 2)
            {
                if(m_settings.StereoMethod == StereoMethod.Independent)
                {
                    AudioSamples.Deinterlace(fsamples + samplesInBuffer,
                                             fsamples + FlakeConstants.MAX_BLOCKSIZE + samplesInBuffer,
                                             src,
                                             block);
                }
                else
                {
                    int* left   = fsamples + samplesInBuffer;
                    int* right  = left     + FlakeConstants.MAX_BLOCKSIZE;
                    int* leftM  = right    + FlakeConstants.MAX_BLOCKSIZE;
                    int* rightM = leftM    + FlakeConstants.MAX_BLOCKSIZE;

                    for(var i = 0; i < block; i++)
                    {
                        int l = src[2 * i];
                        int r = src[2 * i + 1];
                        left[i]   = l;
                        right[i]  = r;
                        leftM[i]  = l + r >> 1;
                        rightM[i] = l - r;
                    }
                }
            }
            else
            {
                for(var ch = 0; ch < channels; ch++)
                {
                    int* psamples = fsamples + ch * FlakeConstants.MAX_BLOCKSIZE + samplesInBuffer;
                    for(var i = 0; i < block; i++) psamples[i] = src[i * channels + ch];
                }
            }
        }

        samplesInBuffer += block;
    }

    //unsafe static void channel_decorrelation(int* leftS, int* rightS, int *leftM, int *rightM, int blocksize)
    //{
    //    for (int i = 0; i < blocksize; i++)
    //    {
    //        leftM[i] = (leftS[i] + rightS[i]) >> 1;
    //        rightM[i] = leftS[i] - rightS[i];
    //    }
    //}

    unsafe void encode_residual_verbatim(int* res, int* smp, uint n)
    {
        AudioSamples.MemCpy(res, smp, (int)n);
    }

    static unsafe ulong encode_residual_fixed_partition(int* res, int* smp, int* end, int order, int* last_errors)
    {
        var sum = 0UL;

        switch(order)
        {
            case 0:
            {
                while(smp < end)
                {
                    int error = *res++ = *smp++;
                    sum += (uint)(error << 1 ^ error >> 31);
                }

                break;
            }
            case 1:
            {
                int last_error_0 = last_errors[0];

                while(smp < end)
                {
                    int error, save;
                    error        =  *smp++;
                    save         =  error;
                    error        -= last_error_0;
                    *res++       =  error;
                    last_error_0 =  save;
                    sum          += (uint)(error << 1 ^ error >> 31);
                }

                last_errors[0] = last_error_0;

                break;
            }
            case 2:
            {
                int last_error_0 = last_errors[0], last_error_1 = last_errors[1];

                while(smp < end)
                {
                    int error, save;
                    error        =  *smp++;
                    save         =  error;
                    error        -= last_error_0;
                    last_error_0 =  save;
                    save         =  error;
                    error        -= last_error_1;
                    *res++       =  error;
                    last_error_1 =  save;
                    sum          += (uint)(error << 1 ^ error >> 31);
                }

                last_errors[0] = last_error_0;
                last_errors[1] = last_error_1;
                ;

                break;
            }
            case 3:
            {
                int last_error_0 = last_errors[0], last_error_1 = last_errors[1], last_error_2 = last_errors[2];

                while(smp < end)
                {
                    int error, save;
                    error        =  *smp++;
                    save         =  error;
                    error        -= last_error_0;
                    last_error_0 =  save;
                    save         =  error;
                    error        -= last_error_1;
                    last_error_1 =  save;
                    save         =  error;
                    error        -= last_error_2;
                    *res++       =  error;
                    last_error_2 =  save;
                    sum          += (uint)(error << 1 ^ error >> 31);
                }

                last_errors[0] = last_error_0;
                last_errors[1] = last_error_1;
                last_errors[2] = last_error_2;

                break;
            }
            case 4:
            {
                int last_error_0 = last_errors[0],
                    last_error_1 = last_errors[1],
                    last_error_2 = last_errors[2],
                    last_error_3 = last_errors[3];

                while(smp < end)
                {
                    int error, save;
                    error        =  *smp++;
                    save         =  error;
                    error        -= last_error_0;
                    last_error_0 =  save;
                    save         =  error;
                    error        -= last_error_1;
                    last_error_1 =  save;
                    save         =  error;
                    error        -= last_error_2;
                    last_error_2 =  save;
                    save         =  error;
                    error        -= last_error_3;
                    *res++       =  error;
                    last_error_3 =  save;
                    sum          += (uint)(error << 1 ^ error >> 31);
                }

                last_errors[0] = last_error_0;
                last_errors[1] = last_error_1;
                last_errors[2] = last_error_2;
                last_errors[3] = last_error_3;

                break;
            }
            default:
                throw new ArgumentOutOfRangeException();
        }

        return sum;
    }

    static unsafe void encode_residual_fixed(int* res, int* smp, int n, int order, ulong* sums, int pmax)
    {
        int* last_errors = stackalloc int[4];
        int* end         = smp + n;
        int* seg_end     = smp + (n >> pmax);

        if(order > 4) throw new ArgumentOutOfRangeException();

        for(var i = 0; i < order; i++)
        {
            int* next_errors                              = stackalloc int[4];
            next_errors[0] = *res++                       = *smp++;
            for(var j = 0; j < i; j++) next_errors[j + 1] = next_errors[j] - last_errors[j];
            for(var j = 0; j <= i; j++) last_errors[j]    = next_errors[j];
        }

        while(smp < end)
        {
            *sums++ =  encode_residual_fixed_partition(res, smp, seg_end, order, last_errors);
            res     += seg_end - smp;
            smp     =  seg_end;
            seg_end += n >> pmax;
        }
    }

#if XXX
        unsafe static int encode_residual_fixed_estimate_best_order(int* res, int* smp, int n, int order)
        {
            int next_error_0, next_error_1, next_error_2, next_error_3, next_error_4;
            int last_error_0, last_error_1, last_error_2, last_error_3;
            int* end = smp + n;
            ulong total_error_0 = 0, total_error_1 = 0, total_error_2 = 0, total_error_3 = 0, total_error_4 = 0;

            if (order == 0)
            {
                AudioSamples.MemCpy(res, smp, n);
                return 0;
            }

            next_error_0 = *(res++) = *(smp++);
            last_error_0 = next_error_0;

            if (order == 1)
            {
                while (smp < end)
                {
                    next_error_0 = *(smp++);
                    next_error_1 = next_error_0 - last_error_0;

                    last_error_0 = next_error_0;

                    total_error_0 += (ulong)((next_error_0 << 1) ^ (next_error_0 >> 31));
                    total_error_1 += (ulong)((next_error_1 << 1) ^ (next_error_1 >> 31));

                    *(res++) = (int)next_error_1;
                }

                if ((total_error_0 < total_error_1))
                    return 0;
                return 1;
            }

            next_error_0 = *(res++) = *(smp++);
            next_error_1 = next_error_0 - last_error_0;
            last_error_0 = next_error_0;
            last_error_1 = next_error_1;

            if (order == 2)
            {
                while (smp < end)
                {
                    next_error_0 = *(smp++);
                    next_error_1 = next_error_0 - last_error_0;
                    next_error_2 = next_error_1 - last_error_1;

                    last_error_0 = next_error_0;
                    last_error_1 = next_error_1;

                    total_error_0 += (ulong)((next_error_0 << 1) ^ (next_error_0 >> 31));
                    total_error_1 += (ulong)((next_error_1 << 1) ^ (next_error_1 >> 31));
                    total_error_2 += (ulong)((next_error_2 << 1) ^ (next_error_2 >> 31));

                    *(res++) = (int)next_error_2;
                }

                if ((total_error_0 < total_error_1) & (total_error_0 < total_error_2))
                    return 0;
                else if ((total_error_1 < total_error_2))
                    return 1;
                return 2;
            }

            next_error_0 = *(res++) = *(smp++);
            next_error_1 = next_error_0 - last_error_0;
            next_error_2 = next_error_1 - last_error_1;
            last_error_0 = next_error_0;
            last_error_1 = next_error_1;
            last_error_2 = next_error_2;

            if (order == 3)
            {
                while (smp < end)
                {
                    next_error_0 = *(smp++);
                    next_error_1 = next_error_0 - last_error_0;
                    next_error_2 = next_error_1 - last_error_1;
                    next_error_3 = next_error_2 - last_error_2;

                    last_error_0 = next_error_0;
                    last_error_1 = next_error_1;
                    last_error_2 = next_error_2;

                    total_error_0 += (ulong)((next_error_0 << 1) ^ (next_error_0 >> 31));
                    total_error_1 += (ulong)((next_error_1 << 1) ^ (next_error_1 >> 31));
                    total_error_2 += (ulong)((next_error_2 << 1) ^ (next_error_2 >> 31));
                    total_error_3 += (ulong)((next_error_3 << 1) ^ (next_error_3 >> 31));

                    *(res++) = (int)next_error_3;
                }

                if ((total_error_0 < total_error_1) & (total_error_0 < total_error_2) & (total_error_0 < total_error_3))
                    return 0;
                else if ((total_error_1 < total_error_2) & (total_error_1 < total_error_3))
                    return 1;
                else if ((total_error_2 < total_error_3))
                    return 2;
                return 3;
            }

            next_error_0 = *(res++) = *(smp++);
            next_error_1 = next_error_0 - last_error_0;
            next_error_2 = next_error_1 - last_error_1;
            next_error_3 = next_error_2 - last_error_2;
            last_error_0 = next_error_0;
            last_error_1 = next_error_1;
            last_error_2 = next_error_2;
            last_error_3 = next_error_3;

            if (order == 4)
            {
                while (smp < end)
                {
                    next_error_0 = *(smp++);
                    next_error_1 = next_error_0 - last_error_0;
                    next_error_2 = next_error_1 - last_error_1;
                    next_error_3 = next_error_2 - last_error_2;
                    next_error_4 = next_error_3 - last_error_3;

                    last_error_0 = next_error_0;
                    last_error_1 = next_error_1;
                    last_error_2 = next_error_2;
                    last_error_3 = next_error_3;

                    total_error_0 += (ulong)((next_error_0 << 1) ^ (next_error_0 >> 31));
                    total_error_1 += (ulong)((next_error_1 << 1) ^ (next_error_1 >> 31));
                    total_error_2 += (ulong)((next_error_2 << 1) ^ (next_error_2 >> 31));
                    total_error_3 += (ulong)((next_error_3 << 1) ^ (next_error_3 >> 31));
                    total_error_4 += (ulong)((next_error_4 << 1) ^ (next_error_4 >> 31));

                    *(res++) = (int)next_error_4;
                }

                if ((total_error_0 < total_error_1) & (total_error_0 < total_error_2) & (total_error_0 < total_error_3) & (total_error_0 < total_error_4))
                    return 0;
                else if ((total_error_1 < total_error_2) & (total_error_1 < total_error_3) & (total_error_1 < total_error_4))
                    return 1;
                else if ((total_error_2 < total_error_3) & (total_error_2 < total_error_4))
                    return 2;
                else if (total_error_3 < total_error_4)
                    return 3;
                return 4;
            }

            throw new ArgumentOutOfRangeException();
        }
#endif
    static unsafe uint calc_optimal_rice_params(int     porder, int* parm, ulong* sums, uint n, uint pred_order,
                                                ref int method)
    {
        uint  part      = 1U << porder;
        uint  cnt       = (n >> porder) - pred_order;
        int   maxK      = method > 0 ? 30 : FlakeConstants.MAX_RICE_PARAM;
        int   k         = cnt    > 0 ? Math.Min(maxK, BitReader.log2i(sums[0] / cnt)) : 0;
        int   realMaxK0 = k;
        ulong all_bits  = cnt * ((uint)k + 1U) + (sums[0] >> k);
        parm[0] = k;
        cnt     = n >> porder;
        int logcnt = BitReader.log2i(cnt);

        if(cnt == 1 << logcnt)
        {
            for(uint i = 1; i < part; i++)
            {
                ulong s = sums[i];
                ulong u = s    >> logcnt;
                k         =  u >> maxK != 0 ? maxK : BitReader.log2i((uint)u);
                realMaxK0 =  Math.Max(realMaxK0, k);
                all_bits  += ((uint)k << logcnt) + (s >> k);
                parm[i]   =  k;
            }
        }
        else
        {
            for(uint i = 1; i < part; i++)
            {
                ulong s = sums[i];
                ulong u = s / cnt;
                k         =  u >> maxK != 0 ? maxK : BitReader.log2i((uint)u);
                realMaxK0 =  Math.Max(realMaxK0, k);
                all_bits  += cnt * (uint)k + (s >> k);
                parm[i]   =  k;
            }
        }

        all_bits += cnt * (part - 1U);
        method   =  realMaxK0 > FlakeConstants.MAX_RICE_PARAM ? 1 : 0;

        return (uint)all_bits + (4U + (uint)method) * part;
    }

    static unsafe void calc_lower_sums(int pmin, int pmax, ulong* sums)
    {
        for(int i = pmax - 1; i >= pmin; i--)
        {
            for(var j = 0; j < 1 << i; j++)
            {
                sums[i * FlakeConstants.MAX_PARTITIONS + j] =
                    sums[(i + 1) * FlakeConstants.MAX_PARTITIONS + 2 * j] +
                    sums[(i + 1) * FlakeConstants.MAX_PARTITIONS + 2 * j + 1];
            }
        }
    }

    static unsafe uint calc_rice_params_sums(RiceContext rc, int pmin, int pmax, ulong* sums, uint n, uint pred_order,
                                             int         bps)
    {
        int* parm = stackalloc int[(pmax + 1) * FlakeConstants.MAX_PARTITIONS];

        //uint* bits = stackalloc uint[FlakeConstants.MAX_PARTITION_ORDER];

        //assert(pmin >= 0 && pmin <= FlakeConstants.MAX_PARTITION_ORDER);
        //assert(pmax >= 0 && pmax <= FlakeConstants.MAX_PARTITION_ORDER);
        //assert(pmin <= pmax);

        // sums for lower levels
        calc_lower_sums(pmin, pmax, sums);

        uint opt_bits   = AudioSamples.UINT32_MAX;
        int  opt_porder = pmin;
        var  opt_method = 0;

        for(int i = pmin; i <= pmax; i++)
        {
            int method = bps > 16 ? 1 : 0;

            uint bits = calc_optimal_rice_params(i,
                                                 parm + i * FlakeConstants.MAX_PARTITIONS,
                                                 sums + i * FlakeConstants.MAX_PARTITIONS,
                                                 n,
                                                 pred_order,
                                                 ref method);

            if(bits <= opt_bits)
            {
                opt_bits   = bits;
                opt_porder = i;
                opt_method = method;
            }
        }

        rc.porder        = opt_porder;
        rc.coding_method = opt_method;

        fixed(int* rparms = rc.rparams)
        {
            AudioSamples.MemCpy(rparms, parm + opt_porder * FlakeConstants.MAX_PARTITIONS, 1 << opt_porder);
        }

        return opt_bits;
    }

    static int get_max_p_order(int max_porder, int n, int order)
    {
        int porder           = Math.Min(max_porder, BitReader.log2i(n ^ n - 1));
        if(order > 0) porder = Math.Min(porder, BitReader.log2i(n / order));

        return porder;
    }

//        private static int[,] best_x = new int[14,8193];
    static readonly int[][] good_x =
    {
        [], // 0
        [   // 1
            0x03, 0x01, 0x00, 0x02
        ],
        [ // 2
            0x01, 0x07, 0x06, 0x02, 0x03, 0x04, 0x00, 0x05
        ],
        [ // 3
            0x0b, 0x0f, 0x0e, 0x0d, 0x03, 0x01, 0x05, 0x02
        ],
        [ //4
            0x17, 0x09, 0x03, 0x0a, 0x06, 0x1d, 0x1f, 0x05, 0x1c, 0x0d, 0x07, 0x0c
        ],
        [ // 5
            0x2b, 0x3d, 0x37, 0x07, 0x11, 0x15, 0x36, 0x3f
        ],
        [ // 6
            0x6b, 0x15, 0x7e, 0x31, 0x07, 0x1a, 0x29, 0x26, 0x5d, 0x23, 0x6f, 0x19, 0x56, 0x75
        ],
        [ // 7
            0xdb, 0xef, 0xb5, 0x47, 0xee, 0x63, 0x0b, 0xfd, 0x31, 0xbe, 0xed, 0x33, 0xff, 0xfb, 0xd6, 0xbb
        ],
        [ // 8
            0x1bb, 0x1c7, 0x069, 0x087, 0x1fd, 0x16e, 0x095, 0x1de, 0x066, 0x071, 0x055, 0x09a
        ],
        [ // 9
            0x36b, 0x3bd, 0x097, 0x0c3, 0x0e3, 0x0b1, 0x107, 0x2de, 0x3ef, 0x2fb, 0x3d5, 0x139
        ],
        [ // 10
//0x0e3,0x199,0x383,0x307, 0x1e3,0x01f,0x269,0x0f1, 0x266,0x03f,0x2cd,0x1c3, 0x19a,0x387,0x339,0x259,
            0x6eb, 0x187, 0x77d, 0x271, 0x195, 0x259, 0x5ae, 0x169
        ],
        [ // 11
            0xddb, 0xf77, 0xb6d, 0x587, 0x2c3, 0x03b, 0xef5, 0x1e3, 0xdbe
        ],
        [ // 12
            0x1aeb, 0x0587, 0x0a71, 0x1dbd, 0x0559, 0x0aa5, 0x0a2e, 0x0d43, 0x05aa, 0x00f3, 0x0696, 0x03c6
        ],
        [                                                                  // 13
            0x35d7, 0x2f6f, 0x0aa3, 0x1569, 0x150f, 0x3d79, 0x0dc3, 0x309f /*?*/
        ],
        [ // 14
            0x75d7, 0x5f7b, 0x6a8f, 0x29a3
        ],
        [ // 15
            0xddd7, 0xaaaf, 0x55c3, 0xf77b
        ],
        [ // 16
            0x1baeb, 0x1efaf, 0x1d5bf, 0x1cff3
        ],
        [ // 17
            0x36dd7, 0x3bb7b, 0x3df6f, 0x2d547
        ],
        [ // 18
            0x75dd7, 0x6f77b, 0x7aaaf, 0x5ddd3
        ],
        [ // 19
            0xdddd7, 0xf777b, 0xd5547, 0xb6ddb
        ],
        [ // 20
            0x1baeeb, 0x1efbaf, 0x1aaabf, 0x17bbeb
        ],
        [ // 21
            0x376dd7, 0x3ddf7b, 0x2d550f, 0x0aaaa3
        ],
        [ // 22
            0x6eddd7, 0x77777b, 0x5dcd4f, 0x5d76f9
        ],
        [ // 23
            0xdeddd7, 0xb5b6eb, 0x55552b, 0x2aaac3
        ],
        [ // 24
            0x1dddbb7, 0x1b76eeb, 0x17bbf5f, 0x1eeaa9f
        ],
        [ // 25
        ],
        [ // 26
        ],
        [ // 27
        ],
        [ // 28
        ],
        [ // 29
        ],
        [ // 30
        ]
    };

    unsafe void postprocess_coefs(FlacFrame frame, FlacSubframe sf, int ch)
    {
        if(eparams.development_mode < 0) return;
        if(sf.type != SubframeType.LPC || sf.order > 30) return;
        int  orig_window = sf.window;
        int  orig_order = sf.order;
        int  orig_shift = sf.shift;
        int  orig_cbits = sf.cbits;
        uint orig_size = sf.size;
        int* orig_coefs = stackalloc int[orig_order];
        for(var i = 0; i < orig_order; i++) orig_coefs[i] = sf.coefs[i];
        int    orig_xx = -1;
        var    orig_seq = 0;
        int    maxxx = Math.Min(good_x[orig_order].Length, eparams.development_mode);
        int    pmax = get_max_p_order(m_settings.MaxPartitionOrder, frame.blocksize, orig_order);
        int    pmin = Math.Min(m_settings.MinPartitionOrder, pmax);
        ulong* sums = stackalloc ulong[(pmax + 1) * FlakeConstants.MAX_PARTITIONS];

        while(true)
        {
            int* best_coefs = stackalloc int[orig_order];
            int  best_shift = orig_shift;
            int  best_cbits = orig_cbits;
            uint best_size  = orig_size;
            int  best_xx    = -1;

            for(int xx = -1; xx < maxxx; xx++)
            {
                int x = xx;

                if(xx < 0)
                {
                    if(orig_xx < 0 || maxxx < 1 /*3*/) // || (orig_xx >> orig_order) != 0)
                        continue;

                    x = orig_xx;
                    orig_seq++;
                }
                else
                {
                    orig_seq = 0;
                    if(orig_order < good_x.Length && good_x[orig_order] != null) x = good_x[orig_order][xx];
                }

                frame.current.type   = SubframeType.LPC;
                frame.current.order  = orig_order;
                frame.current.window = orig_window;
                frame.current.shift  = orig_shift;
                frame.current.cbits  = orig_cbits;

                if((x >> orig_order & 1) != 0)
                {
                    frame.current.shift--;
                    frame.current.cbits--;

                    if(frame.current.shift < 0 || frame.current.cbits < 2) continue;
                }

                ulong csum = 0;
                int   qmax = (1 << frame.current.cbits - 1) - 1;

                for(var i = 0; i < frame.current.order; i++)
                {
                    int shift     = x >> orig_order & 1;
                    int increment = x == 1 << orig_order ? 0 : ((x >> i & 1) << 1) - 1;
                    frame.current.coefs[i] = orig_coefs[i] + (increment << orig_seq) >> shift;
                    if(frame.current.coefs[i] < -(qmax + 1)) frame.current.coefs[i] = -(qmax + 1);
                    if(frame.current.coefs[i] > qmax) frame.current.coefs[i]        = qmax;
                    csum += (ulong)Math.Abs(frame.current.coefs[i]);
                }

                fixed(int* coefs = frame.current.coefs)
                {
                    if(csum << frame.subframes[ch].obits >= 1UL << 32)
                    {
                        lpc.encode_residual_long(frame.current.residual,
                                                 frame.subframes[ch].samples,
                                                 frame.blocksize,
                                                 frame.current.order,
                                                 coefs,
                                                 frame.current.shift,
                                                 sums + pmax * FlakeConstants.MAX_PARTITIONS,
                                                 pmax);
                    }
                    else
                    {
                        lpc.encode_residual(frame.current.residual,
                                            frame.subframes[ch].samples,
                                            frame.blocksize,
                                            frame.current.order,
                                            coefs,
                                            frame.current.shift,
                                            sums + pmax * FlakeConstants.MAX_PARTITIONS,
                                            pmax);
                    }
                }

                uint cur_size = calc_rice_params_sums(frame.current.rc,
                                                      pmin,
                                                      pmax,
                                                      sums,
                                                      (uint)frame.blocksize,
                                                      (uint)frame.current.order,
                                                      Settings.PCM.BitsPerSample);

                frame.current.size = (uint)(frame.current.order * frame.subframes[ch].obits +
                                            4                                               +
                                            5                                               +
                                            frame.current.order * frame.current.cbits       +
                                            6                                               +
                                            (int)cur_size);

                if(frame.current.size < best_size)
                {
                    //var dif = best_size - frame.current.size;
                    for(var i = 0; i < frame.current.order; i++) best_coefs[i] = frame.current.coefs[i];
                    best_shift = frame.current.shift;
                    best_cbits = frame.current.cbits;
                    best_size  = frame.current.size;
                    best_xx    = x;
                    frame.ChooseBestSubframe(ch);

                    //if (dif > orig_order * 5)
                    //    break;
                }

                if(xx < 0 && best_size < orig_size) break;
            }

            if(best_size < orig_size)
            {
                //if (best_xx >= 0) best_x[order, best_xx]++;
                //if (orig_size != 0x7FFFFFFF)
                //    System.Console.Write(string.Format(" {0}[{1:x}]", orig_size - best_size, best_xx));
                for(var i = 0; i < orig_order; i++) orig_coefs[i] = best_coefs[i];
                orig_shift = best_shift;
                orig_cbits = best_cbits;
                orig_size  = best_size;
                orig_xx    = best_xx;
            }
            else
                break;
        }

        //if (orig_size != 0x7FFFFFFF)
        //    System.Console.WriteLine();

        //if (frame_count % 0x400 == 0)
        //{
        //    for (int o = 0; o < best_x.GetLength(0); o++)
        //    {
        //        //for (int x = 0; x <= (1 << o); x++)
        //        //    if (best_x[o, x] != 0)
        //        //        System.Console.WriteLine(string.Format("{0:x2}\t{1:x4}\t{2}", o, x, best_x[o, x]));
        //        var s = new List<KeyValuePair<int, int>>();
        //        for (int x = 0; x < (1 << o); x++)
        //            if (best_x[o, x] != 0)
        //                s.Add(new KeyValuePair<int, int>(x, best_x[o, x]));
        //        s.Sort((x, y) => y.Value.CompareTo(x.Value));
        //        foreach (var x in s)
        //            System.Console.WriteLine(string.Format("{0:x2}\t{1:x4}\t{2}", o, x.Key, x.Value));
        //        int i = 0;
        //        foreach (var x in s)
        //        {
        //            System.Console.Write(string.Format(o <= 8 ? "0x{0:x2}," : "0x{0:x3},", x.Key));
        //            if ((++i) % 16 == 0)
        //                System.Console.WriteLine();
        //        }
        //        System.Console.WriteLine();
        //    }
        //}
    }

    public static void SetCoefs(int order, int[] coefs)
    {
        good_x[order] = new int[coefs.Length];
        for(var i = 0; i < coefs.Length; i++) good_x[order][i] = coefs[i];
    }

    unsafe void encode_residual_lpc_sub(FlacFrame frame, float* lpcs, int iWindow, int order, int ch)
    {
        // select LPC precision based on block size
        uint lpc_precision;

        if(frame.blocksize <= 192)
            lpc_precision = 7U;
        else if(frame.blocksize <= 384)
            lpc_precision = 8U;
        else if(frame.blocksize <= 576)
            lpc_precision = 9U;
        else if(frame.blocksize <= 1152)
            lpc_precision = 10U;
        else if(frame.blocksize <= 2304)
            lpc_precision = 11U;
        else if(frame.blocksize <= 4608)
            lpc_precision = 12U;
        else if(frame.blocksize <= 8192)
            lpc_precision = 13U;
        else if(frame.blocksize <= 16384)
            lpc_precision = 14U;
        else
            lpc_precision = 15;

        for(int i_precision = m_settings.MinPrecisionSearch;
            i_precision <= m_settings.MaxPrecisionSearch && lpc_precision + i_precision < 16;
            i_precision++)

            // check if we already calculated with this order, window and precision
        {
            if((frame.subframes[ch].lpc_ctx[iWindow].done_lpcs[i_precision] & 1U << order - 1) == 0)
            {
                frame.subframes[ch].lpc_ctx[iWindow].done_lpcs[i_precision] |= 1U << order - 1;

                uint cbits = lpc_precision + (uint)i_precision;

                frame.current.type   = SubframeType.LPC;
                frame.current.order  = order;
                frame.current.window = iWindow;
                frame.current.cbits  = (int)cbits;

                int    pmax = get_max_p_order(m_settings.MaxPartitionOrder, frame.blocksize, frame.current.order);
                int    pmin = Math.Min(m_settings.MinPartitionOrder, pmax);
                ulong* sums = stackalloc ulong[(pmax + 1) * FlakeConstants.MAX_PARTITIONS];
                ulong  csum = 0;

                fixed(int* coefs = frame.current.coefs)
                {
                    lpc.quantize_lpc_coefs(lpcs + (frame.current.order - 1) * lpc.MAX_LPC_ORDER,
                                           frame.current.order,
                                           cbits,
                                           coefs,
                                           out frame.current.shift,
                                           15,
                                           0);

                    if(frame.current.shift < 0 || frame.current.shift > 15) throw new Exception("negative shift");

                    for(int i = frame.current.order; i > 0; i--) csum += (ulong)Math.Abs(coefs[i - 1]);

                    if(csum << frame.subframes[ch].obits >= 1UL << 32)
                    {
                        lpc.encode_residual_long(frame.current.residual,
                                                 frame.subframes[ch].samples,
                                                 frame.blocksize,
                                                 frame.current.order,
                                                 coefs,
                                                 frame.current.shift,
                                                 sums + pmax * FlakeConstants.MAX_PARTITIONS,
                                                 pmax);
                    }
                    else
                    {
                        lpc.encode_residual(frame.current.residual,
                                            frame.subframes[ch].samples,
                                            frame.blocksize,
                                            frame.current.order,
                                            coefs,
                                            frame.current.shift,
                                            sums + pmax * FlakeConstants.MAX_PARTITIONS,
                                            pmax);
                    }
                }

                uint best_size = calc_rice_params_sums(frame.current.rc,
                                                       pmin,
                                                       pmax,
                                                       sums,
                                                       (uint)frame.blocksize,
                                                       (uint)frame.current.order,
                                                       Settings.PCM.BitsPerSample);

                frame.current.size = (uint)(frame.current.order * frame.subframes[ch].obits +
                                            4                                               +
                                            5                                               +
                                            frame.current.order * (int)cbits                +
                                            6                                               +
                                            (int)best_size);

                frame.ChooseBestSubframe(ch);

                //if (frame.current.size >= frame.subframes[ch].best.size)
                //    postprocess_coefs(frame, frame.current, ch);
                //else
                //{
                //    frame.ChooseBestSubframe(ch);
                //    postprocess_coefs(frame, frame.subframes[ch].best, ch);
                //}
            }
        }
    }

    unsafe void encode_residual_fixed_sub(FlacFrame frame, int order, int ch)
    {
        if((frame.subframes[ch].done_fixed & 1U << order) != 0) return; // already calculated;

        frame.current.order = order;
        frame.current.type  = SubframeType.Fixed;

#if XXX
            int best_order = order;
            if (frame.subframes[ch].done_fixed == 0)
            {
                best_order =
 encode_residual_fixed_estimate_best_order(frame.current.residual, frame.subframes[ch].samples, frame.blocksize, frame.current.order);
                if (best_order != order)
                {
                    //frame.subframes[ch].done_fixed |= (1U << order);
                    order = best_order;
                    frame.current.order = order;
                    encode_residual_fixed(frame.current.residual, frame.subframes[ch].samples, frame.blocksize, frame.current.order);
                }
            }
            else
#endif
        int    pmax = get_max_p_order(m_settings.MaxPartitionOrder, frame.blocksize, frame.current.order);
        int    pmin = Math.Min(m_settings.MinPartitionOrder, pmax);
        ulong* sums = stackalloc ulong[(pmax + 1) * FlakeConstants.MAX_PARTITIONS];

        encode_residual_fixed(frame.current.residual,
                              frame.subframes[ch].samples,
                              frame.blocksize,
                              frame.current.order,
                              sums + pmax * FlakeConstants.MAX_PARTITIONS,
                              pmax);

        frame.current.size = (uint)(frame.current.order * frame.subframes[ch].obits) +
                             6                                                       +
                             calc_rice_params_sums(frame.current.rc,
                                                   pmin,
                                                   pmax,
                                                   sums,
                                                   (uint)frame.blocksize,
                                                   (uint)frame.current.order,
                                                   Settings.PCM.BitsPerSample);

        frame.subframes[ch].done_fixed |= 1U << order;

        frame.ChooseBestSubframe(ch);
    }

    unsafe void
        fixed_compute_best_predictor(int* data, uint data_len, ulong* errors) //, float* residual_bits_per_sample)
    {
        long  last_error_0  = data[-1];
        long  last_error_1  = data[-1]     - data[-2];
        long  last_error_2  = last_error_1 - (data[-2]                - data[-3]);
        long  last_error_3  = last_error_2 - (data[-2] - 2 * data[-3] + data[-4]);
        ulong total_error_0 = 0, total_error_1 = 0, total_error_2 = 0, total_error_3 = 0, total_error_4 = 0;

#if VARIANT1
        long error, save;
        int* finish = data + data_len;

        while(data < finish)
        {
            error         =  *data++;
            total_error_0 += (ulong)(error << 1 ^ error >> 63);
            save          =  error;
            error         -= last_error_0;
            total_error_1 += (ulong)(error << 1 ^ error >> 63);
            last_error_0  =  save;
            save          =  error;
            error         -= last_error_1;
            total_error_2 += (ulong)(error << 1 ^ error >> 63);
            last_error_1  =  save;
            save          =  error;
            error         -= last_error_2;
            total_error_3 += (ulong)(error << 1 ^ error >> 63);
            last_error_2  =  save;
            save          =  error;
            error         -= last_error_3;
            total_error_4 += (ulong)(error << 1 ^ error >> 63);
            last_error_3  =  save;
        }
#else
            int* finish = data + data_len;
            while (data < finish)
            {
                long next_error_0 = *(data++);
                long next_error_1 = next_error_0 - last_error_0;
                long next_error_2 = next_error_1 - last_error_1;
                long next_error_3 = next_error_2 - last_error_2;
                long next_error_4 = next_error_3 - last_error_3;

                last_error_0 = next_error_0;
                last_error_1 = next_error_1;
                last_error_2 = next_error_2;
                last_error_3 = next_error_3;

                total_error_0 += (ulong)((last_error_0 << 1) ^ (last_error_0 >> 63));
                total_error_1 += (ulong)((last_error_1 << 1) ^ (last_error_1 >> 63));
                total_error_2 += (ulong)((last_error_2 << 1) ^ (last_error_2 >> 63));
                total_error_3 += (ulong)((last_error_3 << 1) ^ (last_error_3 >> 63));
                total_error_4 += (ulong)((next_error_4 << 1) ^ (next_error_4 >> 63));
            }
#endif

        errors[0] = total_error_0;
        errors[1] = total_error_1;
        errors[2] = total_error_2;
        errors[3] = total_error_3;
        errors[4] = total_error_4;

        //residual_bits_per_sample[0] = (float)((total_error_0 > 0) ? log(M_LN2 * (FLAC__double)total_error_0 / (FLAC__double)data_len) / M_LN2 : 0.0);
        //residual_bits_per_sample[1] = (float)((total_error_1 > 0) ? log(M_LN2 * (FLAC__double)total_error_1 / (FLAC__double)data_len) / M_LN2 : 0.0);
        //residual_bits_per_sample[2] = (float)((total_error_2 > 0) ? log(M_LN2 * (FLAC__double)total_error_2 / (FLAC__double)data_len) / M_LN2 : 0.0);
        //residual_bits_per_sample[3] = (float)((total_error_3 > 0) ? log(M_LN2 * (FLAC__double)total_error_3 / (FLAC__double)data_len) / M_LN2 : 0.0);
        //residual_bits_per_sample[4] = (float)((total_error_4 > 0) ? log(M_LN2 * (FLAC__double)total_error_4 / (FLAC__double)data_len) / M_LN2 : 0.0);
    }

    unsafe int fixed_compute_best_predictor_order(ulong* error)
    {
        int order;

        if(error[0] < error[1] & error[0] < error[2] & error[0] < error[3] & error[0] < error[4])
            order = 0;
        else if(error[1] < error[2] & error[1] < error[3] & error[1] < error[4])
            order = 1;
        else if(error[2] < error[3] & error[2] < error[4])
            order = 2;
        else if(error[3] < error[4])
            order = 3;
        else
            order = 4;

        return order;
    }

    unsafe void encode_residual(FlacFrame frame, int ch, PredictionType predict, OrderMethod omethod, int pass,
                                int       windows_mask)
    {
        int* smp  = frame.subframes[ch].samples;
        int  i, n = frame.blocksize;

        // save best.window, because we can overwrite it later with fixed frame

        // CONSTANT
        for(i = 1; i < n; i++)
        {
            if(smp[i] != smp[0]) break;
        }

        if(i == n)
        {
            frame.subframes[ch].best.type        = SubframeType.Constant;
            frame.subframes[ch].best.residual[0] = smp[0];
            frame.subframes[ch].best.size        = (uint)frame.subframes[ch].obits;

            return;
        }

        // VERBATIM
        frame.current.type = SubframeType.Verbatim;
        frame.current.size = (uint)(frame.subframes[ch].obits * frame.blocksize);
        frame.ChooseBestSubframe(ch);

        if(n < 5 || predict == PredictionType.None) return;

        // LPC
        if(n > m_settings.MaxLPCOrder && (predict == PredictionType.Levinson || predict == PredictionType.Search)

           //predict == PredictionType.Search ||
           //(pass == 2 && frame.subframes[ch].best.type == SubframeType.LPC))
          )
        {
            float* lpcs      = stackalloc float[lpc.MAX_LPC_ORDER * lpc.MAX_LPC_ORDER];
            int    min_order = m_settings.MinLPCOrder;
            int    max_order = m_settings.MaxLPCOrder;

            for(var iWindow = 0; iWindow < _windowcount; iWindow++)
            {
                if((windows_mask & 1 << iWindow) == 0) continue;

                LpcContext lpc_ctx = frame.subframes[ch].lpc_ctx[iWindow];

                fixed(LpcWindowSection* sections = &windowSections[frame.nSeg, iWindow, 0])
                {
                    lpc_ctx.GetReflection(frame.subframes[ch].sf,
                                          max_order,
                                          frame.blocksize,
                                          smp,
                                          frame.window_buffer + iWindow * FlakeConstants.MAX_BLOCKSIZE * 2,
                                          sections);
                }

                lpc_ctx.ComputeLPC(lpcs);

                //int frameSize = n;
                //float* F = stackalloc float[frameSize];
                //float* B = stackalloc float[frameSize];
                //float* PE = stackalloc float[max_order + 1];
                //float* arp = stackalloc float[max_order];
                //float* rc = stackalloc float[max_order];

                //for (int j = 0; j < frameSize; j++)
                //    F[j] = B[j] = smp[j];

                //for (int K = 1; K <= max_order; K++)
                //{
                //    // BURG:
                //    float denominator = 0.0f;
                //    //float denominator = F[K - 1] * F[K - 1] + B[frameSize - K] * B[frameSize - K];
                //    for (int j = 0; j < frameSize - K; j++)
                //        denominator += F[j + K] * F[j + K] + B[j] * B[j];
                //    denominator /= 2;

                //    // Estimate error
                //    PE[K - 1] = denominator / (frameSize - K);

                //    float reflectionCoeff = 0.0f;
                //    for (int j = 0; j < frameSize - K; j++)
                //        reflectionCoeff += F[j + K] * B[j];
                //    reflectionCoeff /= denominator;
                //    rc[K - 1] = arp[K - 1] = reflectionCoeff;

                //    // Levinson-Durbin
                //    for (int j = 0; j < (K - 1) >> 1; j++)
                //    {
                //        float arptmp = arp[j];
                //        arp[j] -= reflectionCoeff * arp[K - 2 - j];
                //        arp[K - 2 - j] -= reflectionCoeff * arptmp;
                //    }
                //    if (((K - 1) & 1) != 0)
                //        arp[(K - 1) >> 1] -= reflectionCoeff * arp[(K - 1) >> 1];

                //    for (int j = 0; j < frameSize - K; j++)
                //    {
                //        float f = F[j + K];
                //        float b = B[j];
                //        F[j + K] = f - reflectionCoeff * b;
                //        B[j] = b - reflectionCoeff * f;
                //    }

                //    for (int j = 0; j < K; j++)
                //        lpcs[(K - 1) * lpc.MAX_LPC_ORDER + j] = (float)arp[j];
                //}

                switch(omethod)
                {
                    case OrderMethod.Akaike:
                        //lpc_ctx.SortOrdersAkaike(frame.blocksize, m_settings.EstimationDepth, max_order, 7.1, 0.0);
                        lpc_ctx.SortOrdersAkaike(frame.blocksize,
                                                 m_settings.EstimationDepth,
                                                 min_order,
                                                 max_order,
                                                 4.5,
                                                 0);

                        break;
                    default:
                        throw new Exception("unknown order method");
                }

                for(i = 0; i < m_settings.EstimationDepth && i < max_order; i++)
                    encode_residual_lpc_sub(frame, lpcs, iWindow, lpc_ctx.best_orders[i], ch);
            }

            postprocess_coefs(frame, frame.subframes[ch].best, ch);
        }

        // FIXED
        if(predict == PredictionType.Fixed               ||
           predict == PredictionType.Search && pass != 1 ||

           //predict == PredictionType.Search ||
           //(pass == 2 && frame.subframes[ch].best.type == SubframeType.Fixed) ||
           n > m_settings.MaxFixedOrder && n <= m_settings.MaxLPCOrder)
        {
            int max_fixed_order = Math.Min(m_settings.MaxFixedOrder, 4);
            int min_fixed_order = Math.Min(m_settings.MinFixedOrder, max_fixed_order);

            if(min_fixed_order == 0 && max_fixed_order == 4)
            {
                fixed(ulong* fixed_errors = frame.subframes[ch].best_fixed)
                {
                    if((frame.subframes[ch].done_fixed & 1U << 5) == 0)
                    {
                        fixed_compute_best_predictor(smp + 4, (uint)n - 4, fixed_errors);
                        frame.subframes[ch].done_fixed |= 1U << 5;
                    }

                    i = fixed_compute_best_predictor_order(fixed_errors);
                    encode_residual_fixed_sub(frame, i, ch);
                }
            }
            else
                for(i = max_fixed_order; i >= min_fixed_order; i--)
                    encode_residual_fixed_sub(frame, i, ch);
        }
    }

    void output_frame_header(FlacFrame frame, BitWriter bitwriter)
    {
        bitwriter.writebits(15, 0x7FFC);
        bitwriter.writebits(1,  eparams.variable_block_size > 0 ? 1 : 0);
        bitwriter.writebits(4,  frame.bs_code0);
        bitwriter.writebits(4,  sr_code0);

        if(frame.ch_mode == ChannelMode.NotStereo)
            bitwriter.writebits(4, ch_code);
        else
            bitwriter.writebits(4, (int)frame.ch_mode);

        bitwriter.writebits(3, bps_code);
        bitwriter.writebits(1, 0);
        bitwriter.write_utf8(frame_count);

        // custom block size
        if(frame.bs_code1 >= 0)
        {
            if(frame.bs_code1 < 256)
                bitwriter.writebits(8, frame.bs_code1);
            else
                bitwriter.writebits(16, frame.bs_code1);
        }

        // custom sample rate
        if(sr_code1 > 0)
        {
            if(sr_code1 < 256)
                bitwriter.writebits(8, sr_code1);
            else
                bitwriter.writebits(16, sr_code1);
        }

        // CRC-8 of frame header
        bitwriter.flush();
        byte crc = crc8.ComputeChecksum(frame_buffer, 0, bitwriter.Length);
        bitwriter.writebits(8, crc);
    }

    unsafe void output_residual(FlacFrame frame, BitWriter bitwriter, FlacSubframeInfo sub)
    {
        // rice-encoded block
        bitwriter.writebits(2, sub.best.rc.coding_method);

        // partition order
        int porder = sub.best.rc.porder;
        int psize  = frame.blocksize >> porder;

        //assert(porder >= 0);
        bitwriter.writebits(4, porder);
        int res_cnt = psize - sub.best.order;

        int rice_len = 4 + sub.best.rc.coding_method;

        // residual
        int j = sub.best.order;

        fixed(byte* fixbuf = &frame_buffer[0])
        {
            for(var p = 0; p < 1 << porder; p++)
            {
                int k = sub.best.rc.rparams[p];
                bitwriter.writebits(rice_len, k);
                if(p == 1) res_cnt = psize;
                int cnt            = Math.Min(res_cnt, frame.blocksize - j);
                bitwriter.write_rice_block_signed(fixbuf, k, sub.best.residual + j, cnt);
                j += cnt;
            }
        }
    }

    unsafe void output_subframe_constant(FlacFrame frame, BitWriter bitwriter, FlacSubframeInfo sub)
    {
        bitwriter.writebits_signed(sub.obits, sub.best.residual[0]);
    }

    unsafe void output_subframe_verbatim(FlacFrame frame, BitWriter bitwriter, FlacSubframeInfo sub)
    {
        int n = frame.blocksize;
        for(var i = 0; i < n; i++) bitwriter.writebits_signed(sub.obits, sub.samples[i]);

        // Don't use residual here, because we don't copy samples to residual for verbatim frames.
    }

    unsafe void output_subframe_fixed(FlacFrame frame, BitWriter bitwriter, FlacSubframeInfo sub)
    {
        // warm-up samples
        for(var i = 0; i < sub.best.order; i++) bitwriter.writebits_signed(sub.obits, sub.best.residual[i]);

        // residual
        output_residual(frame, bitwriter, sub);
    }

    unsafe void output_subframe_lpc(FlacFrame frame, BitWriter bitwriter, FlacSubframeInfo sub)
    {
        // warm-up samples
        for(var i = 0; i < sub.best.order; i++) bitwriter.writebits_signed(sub.obits, sub.best.residual[i]);

        // LPC coefficients
        var cbits = 1;

        for(var i = 0; i < sub.best.order; i++)
            while(cbits < 16 && sub.best.coefs[i] != sub.best.coefs[i] << 32 - cbits >> 32 - cbits)
                cbits++;

        bitwriter.writebits(4, cbits - 1);
        bitwriter.writebits_signed(5, sub.best.shift);
        for(var i = 0; i < sub.best.order; i++) bitwriter.writebits_signed(cbits, sub.best.coefs[i]);

        // residual
        output_residual(frame, bitwriter, sub);
    }

    void output_subframes(FlacFrame frame, BitWriter bitwriter)
    {
        for(var ch = 0; ch < channels; ch++)
        {
            FlacSubframeInfo sub = frame.subframes[ch];

            // subframe header
            var type_code                                     = (int)sub.best.type;
            if(sub.best.type == SubframeType.Fixed) type_code |= sub.best.order;
            if(sub.best.type == SubframeType.LPC) type_code   |= sub.best.order - 1;
            bitwriter.writebits(1, 0);
            bitwriter.writebits(6, type_code);
            bitwriter.writebits(1, sub.wbits != 0 ? 1 : 0);
            if(sub.wbits > 0) bitwriter.writebits(sub.wbits, 1);

            // subframe
            switch(sub.best.type)
            {
                case SubframeType.Constant:
                    output_subframe_constant(frame, bitwriter, sub);

                    break;
                case SubframeType.Verbatim:
                    output_subframe_verbatim(frame, bitwriter, sub);

                    break;
                case SubframeType.Fixed:
                    output_subframe_fixed(frame, bitwriter, sub);

                    break;
                case SubframeType.LPC:
                    output_subframe_lpc(frame, bitwriter, sub);

                    break;
            }
        }
    }

    void output_frame_footer(BitWriter bitwriter)
    {
        bitwriter.flush();
        ushort crc = bitwriter.get_crc16();
        bitwriter.writebits(16, crc);
        bitwriter.flush();
    }

    void encode_residual_pass1(FlacFrame frame, int ch, int windows_mask)
    {
        int max_prediction_order = m_settings.MaxLPCOrder;

        //int max_fixed_order = m_settings.MaxFixedOrder;
        //int min_fixed_order = m_settings.MinFixedOrder;
        int lpc_min_precision_search = m_settings.MinPrecisionSearch;
        int lpc_max_precision_search = m_settings.MaxPrecisionSearch;
        int max_partition_order      = m_settings.MaxPartitionOrder;
        int estimation_depth         = m_settings.EstimationDepth;
        int development_mode         = eparams.development_mode;

        //m_settings.MinFixedOrder = 2;
        //m_settings.MaxFixedOrder = 2;
        m_settings.MinPrecisionSearch = m_settings.MaxPrecisionSearch;
        m_settings.MaxLPCOrder        = Math.Min(m_settings.MaxLPCOrder, Math.Max(m_settings.MinLPCOrder, 8));
        m_settings.EstimationDepth    = 1;
        eparams.development_mode      = Math.Min(eparams.development_mode, -1);
        encode_residual(frame, ch, m_settings.PredictionType, OrderMethod.Akaike, 1, windows_mask);

        //m_settings.MinFixedOrder = min_fixed_order;
        //m_settings.MaxFixedOrder = max_fixed_order;
        m_settings.MaxLPCOrder        = max_prediction_order;
        m_settings.MinPrecisionSearch = lpc_min_precision_search;
        m_settings.MaxPrecisionSearch = lpc_max_precision_search;
        m_settings.MaxPartitionOrder  = max_partition_order;
        m_settings.EstimationDepth    = estimation_depth;
        eparams.development_mode      = development_mode;
    }

    void encode_residual_pass2(FlacFrame frame, int ch)
    {
        encode_residual(frame,
                        ch,
                        m_settings.PredictionType,
                        eparams.order_method,
                        2,
                        estimate_best_windows(frame, ch));
    }

    unsafe int estimate_best_windows_akaike(FlacFrame frame, int ch, int count, bool onePerType)
    {
        int* windows_present                                     = stackalloc int[_windowcount];
        for(var i = 0; i < _windowcount; i++) windows_present[i] = 0;

        if(onePerType)
        {
            for(var i = 0; i < _windowcount; i++)
            {
                for(var j = 0; j < _windowcount; j++)
                    if(windowType[j] == windowType[i])
                        windows_present[j]++;
            }
        }

        float* err = stackalloc float[lpc.MAX_LPC_ORDER];

        for(var i = 0; i < _windowcount; i++)
        {
            LpcContext lpc_ctx = frame.subframes[ch].lpc_ctx[i];

            if(onePerType && windows_present[i] <= count)
            {
                err[i] = 0;

                continue;
            }

            var estimate_order = 4;

            fixed(LpcWindowSection* sections = &windowSections[frame.nSeg, i, 0])
            {
                lpc_ctx.GetReflection(frame.subframes[ch].sf,
                                      estimate_order,
                                      frame.blocksize,
                                      frame.subframes[ch].samples,
                                      frame.window_buffer + i * FlakeConstants.MAX_BLOCKSIZE * 2,
                                      sections);
            }

            lpc_ctx.SortOrdersAkaike(frame.blocksize, 1, 1, estimate_order, 4.5, 0.0);

            //err[i] = (float)(lpc_ctx.Akaike(frame.blocksize, lpc_ctx.best_orders[0], 4.5, 0.0));
            //err[i] = (float)((frame.blocksize * lpc_ctx.prediction_error[lpc_ctx.best_orders[0] - 1] / windowScale[i]) + lpc_ctx.best_orders[0] * 4.5);
            //err[i] = (float)((frame.blocksize * lpc_ctx.prediction_error[lpc_ctx.best_orders[0] - 1] / windowScale[i]) + lpc_ctx.best_orders[0] * frame.subframes[ch].obits);

            // realistic
            //err[i] = (float)(frame.blocksize * Math.Log(lpc_ctx.prediction_error[lpc_ctx.best_orders[0] - 1]) / Math.Log(2) / 2.5
            //- windowScale[i] / 2 + lpc_ctx.best_orders[0] * frame.subframes[ch].obits / 2);

            //err[i] = (float)(frame.blocksize * Math.Log(lpc_ctx.prediction_error[lpc_ctx.best_orders[0] - 1]) / Math.Log(2) / 2.5
            //- frame.blocksize * Math.Log(lpc_ctx.autocorr_values[0]) / 2.1
            //+ Math.Log(frame.blocksize) * lpc_ctx.best_orders[0] * 4.5 / 2.5 / Math.Log(2));

            // Akaike
            //err[i] = (float)(frame.blocksize * (Math.Log(lpc_ctx.prediction_error[lpc_ctx.best_orders[0] - 1])) + Math.Log(frame.blocksize) * lpc_ctx.best_orders[0] * 4.5);

            //err[i] = (float)(lpc_ctx.Akaike(frame.blocksize, lpc_ctx.best_orders[0], 4.5, 0.0) - frame.blocksize * (frame.subframes[ch].obits + Math.Log(windowScale[i] / frame.blocksize) / 2));

            // tested good
            err[i] = (float)(lpc_ctx.Akaike(frame.blocksize, lpc_ctx.best_orders[0], 4.5, 0.0) -
                             frame.blocksize * Math.Log(lpc_ctx.autocorr_values[0]) / 2);
        }

        int* best_windows                                     = stackalloc int[lpc.MAX_LPC_ORDER];
        for(var i = 0; i < _windowcount; i++) best_windows[i] = i;

        for(var i = 0; i < _windowcount; i++)
        {
            for(int j = i + 1; j < _windowcount; j++)
            {
                if(err[best_windows[i]] > err[best_windows[j]])
                {
                    int tmp = best_windows[j];
                    best_windows[j] = best_windows[i];
                    best_windows[i] = tmp;
                }
            }
        }

        var window_mask = 0;

        if(onePerType)
        {
            for(var i = 0; i < _windowcount; i++) windows_present[i] = count;

            for(var i = 0; i < _windowcount; i++)
            {
                int w = best_windows[i];

                if(windows_present[w] > 0)
                {
                    for(var j = 0; j < _windowcount; j++)
                        if(windowType[j] == windowType[w])
                            windows_present[j]--;

                    window_mask |= 1 << w;
                }
            }
        }
        else
            for(var i = 0; i < _windowcount && i < count; i++)
                window_mask |= 1 << best_windows[i];

        return window_mask;
    }

    int estimate_best_windows(FlacFrame frame, int ch)
    {
        if(_windowcount == 1 || m_settings.PredictionType == PredictionType.Fixed) return 1;

        switch(m_settings.WindowMethod)
        {
            case WindowMethod.Estimate:
                return estimate_best_windows_akaike(frame, ch, 1, false);
            case WindowMethod.Estimate2:
                return estimate_best_windows_akaike(frame, ch, 2, false);
            case WindowMethod.Estimate3:
                return estimate_best_windows_akaike(frame, ch, 3, false);
            case WindowMethod.EstimateN:
                return estimate_best_windows_akaike(frame, ch, 1, true);
            case WindowMethod.Evaluate2:
                encode_residual_pass1(frame, ch, estimate_best_windows_akaike(frame, ch, 2, false));

                return frame.subframes[ch].best.type == SubframeType.LPC ? 1 << frame.subframes[ch].best.window : 0;
            case WindowMethod.Evaluate3:
                encode_residual_pass1(frame, ch, estimate_best_windows_akaike(frame, ch, 3, false));

                return frame.subframes[ch].best.type == SubframeType.LPC ? 1 << frame.subframes[ch].best.window : 0;
            case WindowMethod.EvaluateN:
                encode_residual_pass1(frame, ch, estimate_best_windows_akaike(frame, ch, 1, true));
#if XXX
                    if (frame.subframes[ch].best.type == SubframeType.LPC && frame.subframes[ch].best.order <= 4)
                    {
                        LpcContext lpc_ctx = frame.subframes[ch].lpc_ctx[frame.subframes[ch].best.window];
                        double err =
 lpc_ctx.prediction_error[frame.subframes[ch].best.order - 1] / lpc_ctx.autocorr_values[0];
                        double est = frame.blocksize * (frame.subframes[ch].obits * (1 - err));
                        double est1 = frame.blocksize * (frame.subframes[ch].obits * (err));
                        if (est < 0 || est1 < 0) return -1;
                    }
#endif
                return frame.subframes[ch].best.type == SubframeType.LPC ? 1 << frame.subframes[ch].best.window : 0;
            case WindowMethod.Evaluate2N:
                encode_residual_pass1(frame, ch, estimate_best_windows_akaike(frame, ch, 2, true));

                return frame.subframes[ch].best.type == SubframeType.LPC ? 1 << frame.subframes[ch].best.window : 0;
            case WindowMethod.Evaluate3N:
                encode_residual_pass1(frame, ch, estimate_best_windows_akaike(frame, ch, 3, true));

                return frame.subframes[ch].best.type == SubframeType.LPC ? 1 << frame.subframes[ch].best.window : 0;
            case WindowMethod.Evaluate:
                encode_residual_pass1(frame, ch, -1);

                return frame.subframes[ch].best.type == SubframeType.LPC ? 1 << frame.subframes[ch].best.window : 0;
            case WindowMethod.Search:
                return -1;
        }

        return -1;
    }

    unsafe void estimate_frame(FlacFrame frame, bool do_midside)
    {
        int subframes = do_midside ? channels * 2 : channels;

        switch(m_settings.StereoMethod)
        {
            case StereoMethod.Estimate:
                for(var ch = 0; ch < subframes; ch++)
                {
                    LpcContext lpc_ctx        = frame.subframes[ch].lpc_ctx[0];
                    var        estimate_order = 4;
                    var        iWindow        = 0;

                    fixed(LpcWindowSection* sections = &windowSections[frame.nSeg, iWindow, 0])
                    {
                        lpc_ctx.GetReflection(frame.subframes[ch].sf,
                                              estimate_order,
                                              frame.blocksize,
                                              frame.subframes[ch].samples,
                                              frame.window_buffer + iWindow * FlakeConstants.MAX_BLOCKSIZE * 2,
                                              sections);
                    }

                    lpc_ctx.SortOrdersAkaike(frame.blocksize, 1, 1, estimate_order, 4.5, 0.0);

                    frame.subframes[ch].best.size = (uint)Math.Max(0,
                                                                   frame.blocksize *
                                                                   Math.Log(lpc_ctx.prediction_error
                                                                                [lpc_ctx.best_orders[0] - 1]) +
                                                                   Math.Log(frame.blocksize) *
                                                                   lpc_ctx.best_orders[0]    *
                                                                   4.5

                                                                   //= (uint)Math.Max(0, lpc_ctx.Akaike(frame.blocksize, lpc_ctx.best_orders[0], 4.5, 0.0)
                                                                   //* 2.0 / Math.Log(windowScale[0] / frame.blocksize)
                                                                  +
                                                                   7.1                       *
                                                                   frame.subframes[ch].obits *
                                                                   m_settings.MaxLPCOrder);
                }

                break;
            case StereoMethod.EstimateFixed:
                for(var ch = 0; ch < subframes; ch++)
                {
                    fixed(ulong* fixed_errors = frame.subframes[ch].best_fixed)
                    {
                        if((frame.subframes[ch].done_fixed & 1U << 5) == 0)
                        {
                            fixed_compute_best_predictor(frame.subframes[ch].samples + 4,
                                                         (uint)frame.blocksize       - 4,
                                                         fixed_errors);

                            frame.subframes[ch].done_fixed |= 1U << 5;
                        }

                        int best_order = fixed_compute_best_predictor_order(fixed_errors);

                        //residual_bits_per_sample[0] = (float)((total_error_0 > 0) ? log(M_LN2 * (FLAC__double)total_error_0 / (FLAC__double)data_len) / M_LN2 : 0.0);
                        frame.subframes[ch].best.size = (uint)fixed_errors[best_order];
                    }
                }

                break;
            case StereoMethod.EstimateX:
                for(var ch = 0; ch < subframes; ch++)
                {
                    for(var iWindow = 0; iWindow < _windowcount; iWindow++)
                    {
                        LpcContext lpc_ctx        = frame.subframes[ch].lpc_ctx[iWindow];
                        var        estimate_order = 4;

                        fixed(LpcWindowSection* sections = &windowSections[frame.nSeg, iWindow, 0])
                        {
                            lpc_ctx.GetReflection(frame.subframes[ch].sf,
                                                  estimate_order,
                                                  frame.blocksize,
                                                  frame.subframes[ch].samples,
                                                  frame.window_buffer + iWindow * FlakeConstants.MAX_BLOCKSIZE * 2,
                                                  sections);
                        }

                        lpc_ctx.SortOrdersAkaike(frame.blocksize, 1, 1, estimate_order, 4.5, 0.0);

                        var estimate = (uint)Math.Max(0,
                                                      frame.blocksize *
                                                      Math.Log(lpc_ctx.prediction_error[lpc_ctx.best_orders[0] - 1]) +
                                                      Math.Log(frame.blocksize) * lpc_ctx.best_orders[0] * 4.5

                                                      //= (uint)Math.Max(0, lpc_ctx.Akaike(frame.blocksize, lpc_ctx.best_orders[0], 4.5, 0.0)
                                                      //* 2.0 / Math.Log(windowScale[0] / frame.blocksize)
                                                     +
                                                      7.1 * frame.subframes[ch].obits * m_settings.MaxLPCOrder);

                        if(iWindow == 0 || frame.subframes[ch].best.size > estimate)
                            frame.subframes[ch].best.size = estimate;
                    }
                }

                break;
            case StereoMethod.Evaluate:
                for(var ch = 0; ch < subframes; ch++) encode_residual_pass1(frame, ch, 1);

                break;
            case StereoMethod.EvaluateX:
                for(var ch = 0; ch < subframes; ch++)
                    encode_residual_pass1(frame, ch, estimate_best_windows_akaike(frame, ch, 1, false));

                break;
            case StereoMethod.Search:
                for(var ch = 0; ch < subframes; ch++) encode_residual_pass2(frame, ch);

                break;
        }
    }

    uint measure_frame_size(FlacFrame frame, bool do_midside)
    {
        // crude estimation of header/footer size
        var total = (uint)(32                                          +
                           (BitReader.log2i(frame_count) + 4) / 5 * 8  +
                           (eparams.variable_block_size != 0 ? 16 : 0) +
                           16);

        if(do_midside)
        {
            uint        bitsBest = AudioSamples.UINT32_MAX;
            ChannelMode modeBest = ChannelMode.LeftRight;

            if(bitsBest > frame.subframes[2].best.size + frame.subframes[3].best.size)
            {
                bitsBest = frame.subframes[2].best.size + frame.subframes[3].best.size;
                modeBest = ChannelMode.MidSide;
            }

            if(bitsBest > frame.subframes[3].best.size + frame.subframes[1].best.size)
            {
                bitsBest = frame.subframes[3].best.size + frame.subframes[1].best.size;
                modeBest = ChannelMode.RightSide;
            }

            if(bitsBest > frame.subframes[3].best.size + frame.subframes[0].best.size)
            {
                bitsBest = frame.subframes[3].best.size + frame.subframes[0].best.size;
                modeBest = ChannelMode.LeftSide;
            }

            if(bitsBest > frame.subframes[0].best.size + frame.subframes[1].best.size)
            {
                bitsBest = frame.subframes[0].best.size + frame.subframes[1].best.size;
                modeBest = ChannelMode.LeftRight;
            }

            frame.ch_mode = modeBest;

            return total + bitsBest;
        }

        for(var ch = 0; ch < channels; ch++) total += frame.subframes[ch].best.size;

        return total;
    }

    void encode_estimated_frame(FlacFrame frame)
    {
        switch(m_settings.StereoMethod)
        {
            case StereoMethod.Estimate:
            case StereoMethod.EstimateX:
            case StereoMethod.EstimateFixed:
                for(var ch = 0; ch < channels; ch++)
                {
                    frame.subframes[ch].best.size = AudioSamples.UINT32_MAX;
                    encode_residual_pass2(frame, ch);
                }

                break;
            case StereoMethod.Evaluate:
            case StereoMethod.EvaluateX:
                for(var ch = 0; ch < channels; ch++) encode_residual_pass2(frame, ch);

                break;
            case StereoMethod.Search:
                break;
        }
    }

    unsafe delegate void window_function(float* window, int size);

    unsafe void calculate_window(float* window, window_function func, WindowFunction flag)
    {
        if((m_settings.WindowFunctions & flag) == 0 || _windowcount == lpc.MAX_LPC_WINDOWS) return;
        int    sz   = _windowsize;
        float* pos1 = window + _windowcount * FlakeConstants.MAX_BLOCKSIZE * 2;
        float* pos  = pos1;
        var    nSeg = 0;

        do
        {
            windowSections[nSeg, _windowcount, 0].setData(0, sz);
            for(var j = 1; j < lpc.MAX_LPC_SECTIONS; j++) windowSections[nSeg, _windowcount, j].setZero(sz, sz);

            fixed(LpcWindowSection* sections = &windowSections[nSeg, _windowcount, 0])
            {
                func(pos, sz);
            }

            if((sz & 1) != 0) break;
            nSeg++;
            pos +=  sz;
            sz  >>= 1;
        } while(sz >= 32);

        var scale                                  = 0.0;
        for(var i = 0; i < _windowsize; i++) scale += pos1[i] * pos1[i];
        windowScale[_windowcount] = scale;
        windowType[_windowcount]  = flag;
        _windowcount++;
    }

    class PunchoutTukeyVariant
    {
        public readonly double         overlap;
        public readonly double         p;
        public readonly int            parts;
        public readonly WindowFunction type;

        public PunchoutTukeyVariant(WindowFunction _type, int _parts, double _overlap, double _p)
        {
            parts   = _parts;
            type    = _type;
            overlap = _overlap;
            p       = _p;
        }
    }

    unsafe int encode_frame(out int size)
    {
        fixed(int* s = samplesBuffer, r = residualBuffer)
        {
            fixed(float* window = windowBuffer)
            {
                frame.InitSize(m_blockSize, eparams.variable_block_size != 0);

                if(frame.blocksize           != _windowsize &&
                   frame.blocksize           > 4            &&
                   m_settings.PredictionType != PredictionType.Fixed)
                {
                    _windowsize  = frame.blocksize;
                    _windowcount = 0;
                    calculate_window(window, lpc.window_welch,    WindowFunction.Welch);
                    calculate_window(window, lpc.window_flattop,  WindowFunction.Flattop);
                    calculate_window(window, lpc.window_hann,     WindowFunction.Hann);
                    calculate_window(window, lpc.window_bartlett, WindowFunction.Bartlett);

                    var tukeys = new PunchoutTukeyVariant[]
                    {
                        new(WindowFunction.Tukey4, 4, 0, 0.03), new(WindowFunction.Tukey4A, 4, 0, 0.03),
                        new(WindowFunction.Tukey4B, 4, 0, 0.03),
                        new(WindowFunction.Tukey4X, 4, m_settings.TukeyOverlap, m_settings.TukeyP),
                        new(WindowFunction.Tukey3, 3, 1.0  / 3, 0.03), new(WindowFunction.Tukey3A, 3, 1.0 / 3, 0.03),
                        new(WindowFunction.Tukey3B, 3, 1.0 / 3, 0.03),
                        new(WindowFunction.Tukey3X, 3, m_settings.TukeyOverlap, m_settings.TukeyP),
                        new(WindowFunction.Tukey2, 2, 0.25, 0.03), new(WindowFunction.Tukey2A, 2, 0.25, 0.03),
                        new(WindowFunction.Tukey2B, 2, 0.25, 0.03),
                        new(WindowFunction.Tukey2X, 2, m_settings.TukeyOverlap, m_settings.TukeyP),
                        new(WindowFunction.Tukey, 1, 0.0, 0.03), new(WindowFunction.Tukey1A, 1, 0.0, 0.03),
                        new(WindowFunction.Tukey1B, 1, 0.0, 0.03),
                        new(WindowFunction.Tukey1X, 1, m_settings.TukeyOverlap, m_settings.TukeyP)
                    };

                    foreach(PunchoutTukeyVariant tukey in tukeys)
                    {
                        if(tukey.parts == 0 || (m_settings.WindowFunctions & tukey.type) == 0) continue;

                        if(tukey.parts == 1)
                        {
                            calculate_window(window, (w, wsz) => { lpc.window_tukey(w, wsz, tukey.p); }, tukey.type);

                            continue;
                        }

                        double overlap       = tukey.overlap;
                        double overlap_units = overlap / (1.0 - overlap);

                        for(var m = 0; m < tukey.parts; m++)
                        {
                            calculate_window(window,
                                             (w, wsz) =>
                                             {
                                                 lpc.window_punchout_tukey(w,
                                                                           wsz,
                                                                           tukey.p,
                                                                           tukey.p,
                                                                           m / (tukey.parts + overlap_units),
                                                                           (m           + 1 + overlap_units) /
                                                                           (tukey.parts + overlap_units));
                                             },
                                             tukey.type);
                        }
                    }

                    if(_windowcount == 0) throw new Exception("invalid windowfunction");
                    var    nSeg           = 0;
                    int    sz             = _windowsize;
                    float* window_segment = window;

                    do
                    {
                        fixed(LpcWindowSection* sections = &windowSections[nSeg, 0, 0])
                        {
                            LpcWindowSection.Detect(_windowcount,
                                                    window_segment,
                                                    FlakeConstants.MAX_BLOCKSIZE * 2,
                                                    sz,
                                                    Settings.PCM.BitsPerSample,
                                                    sections);
                        }

                        if((sz & 1) != 0) break;
                        window_segment += sz;
                        nSeg++;
                        sz >>= 1;
                    } while(sz >= 32);
#if NONONO
                    using (TextWriter tx = File.CreateText(@"H:\ubuntu\flac\w.txt"))
                    {
#if !NONONO
                        int totaltotal = 0;
                        for (int i = 0; i < _windowcount; i++)
                        {
                            int total = 0;
                            for (int sec = 0; sec < lpc.MAX_LPC_SECTIONS; sec++)
                                if (windowSections[0, i, sec].m_type != LpcWindowSection.SectionType.Zero || windowSections[0, i, sec].m_start != windowSections[0, i, sec].m_end)
                                {
                                    tx.WriteLine("{0}\t{1}\t{2}\t{3}", windowSections[0, i, sec].m_start, windowSections[0, i, sec].m_end, windowSections[0, i, sec].m_type, windowSections[0, i, sec].m_id);
                                    if (windowSections[0, i, sec].m_type != LpcWindowSection.SectionType.One)
                                        total += windowSections[0, i, sec].m_end - windowSections[0, i, sec].m_start;
                                }
                            totaltotal += total;
                            tx.WriteLine("{0} total window data", total);
                        }
                        tx.WriteLine("{0} grand total window data", totaltotal);
#endif
                        for (int x = 0; x < frame.blocksize; x++)
                        {
                            tx.Write("{0}", x);
                            for (int i = 0; i < _windowcount; i++)
                                tx.Write("\t{0}", window[i * FlakeConstants.MAX_BLOCKSIZE * 2 + x]);
                            tx.WriteLine();
                        }
                    }
#endif
                }

                if(channels != 2 || frame.blocksize <= 32 || m_settings.StereoMethod == StereoMethod.Independent)
                {
                    frame.window_buffer    = window;
                    frame.nSeg             = 0;
                    frame.current.residual = r + channels * FlakeConstants.MAX_BLOCKSIZE;
                    frame.ch_mode          = channels != 2 ? ChannelMode.NotStereo : ChannelMode.LeftRight;

                    for(var ch = 0; ch < channels; ch++)
                    {
                        frame.subframes[ch]
                             .Init(s + ch * FlakeConstants.MAX_BLOCKSIZE,
                                   r + ch * FlakeConstants.MAX_BLOCKSIZE,
                                   Settings.PCM.BitsPerSample,
                                   get_wasted_bits(s + ch * FlakeConstants.MAX_BLOCKSIZE, frame.blocksize));
                    }

                    for(var ch = 0; ch < channels; ch++) encode_residual_pass2(frame, ch);
                }
                else
                {
                    //channel_decorrelation(s, s + FlakeConstants.MAX_BLOCKSIZE, s + 2 * FlakeConstants.MAX_BLOCKSIZE, s + 3 * FlakeConstants.MAX_BLOCKSIZE, frame.blocksize);
                    frame.window_buffer    = window;
                    frame.nSeg             = 0;
                    frame.current.residual = r + 4 * FlakeConstants.MAX_BLOCKSIZE;

                    for(var ch = 0; ch < 4; ch++)
                    {
                        frame.subframes[ch]
                             .Init(s                          + ch * FlakeConstants.MAX_BLOCKSIZE,
                                   r                          + ch * FlakeConstants.MAX_BLOCKSIZE,
                                   Settings.PCM.BitsPerSample + (ch == 3 ? 1 : 0),
                                   get_wasted_bits(s + ch * FlakeConstants.MAX_BLOCKSIZE, frame.blocksize));
                    }

                    //for (int ch = 0; ch < 4; ch++)
                    //    for (int iWindow = 0; iWindow < _windowcount; iWindow++)
                    //        frame.subframes[ch].lpc_ctx[iWindow].GetReflection(32, frame.subframes[ch].samples, frame.blocksize, frame.window_buffer + iWindow * FlakeConstants.MAX_BLOCKSIZE * 2);

                    estimate_frame(frame, true);
                    uint fs = measure_frame_size(frame, true);

                    if(0 != eparams.variable_block_size)
                    {
                        var frame2  = new FlacFrame(channels * 2);
                        var frame3  = new FlacFrame(channels * 2);
                        var tumbler = 1;

                        while((frame.blocksize & 1) == 0 && frame.blocksize >= 1024)
                        {
                            frame2.InitSize(frame.blocksize / 2, true);
                            frame2.window_buffer    = frame.window_buffer + frame.blocksize;
                            frame2.nSeg             = frame.nSeg          + 1;
                            frame2.current.residual = r                   + tumbler * 5 * FlakeConstants.MAX_BLOCKSIZE;

                            for(var ch = 0; ch < 4; ch++)
                            {
                                frame2.subframes[ch]
                                      .Init(frame.subframes[ch].samples,
                                            frame2.current.residual   + (ch + 1) * frame2.blocksize,
                                            frame.subframes[ch].obits + frame.subframes[ch].wbits,
                                            frame.subframes[ch].wbits);
                            }

                            estimate_frame(frame2, true);

                            //measure_frame_size(frame2, true);
                            //frame2.ChooseSubframes();
                            //encode_estimated_frame(frame2);
                            //uint fs2 = measure_frame_size(frame2, false);
                            uint fs2 = measure_frame_size(frame2, true);
                            uint fs3 = fs2;

                            if(eparams.variable_block_size == 2 || eparams.variable_block_size == 4)
                            {
                                frame3.InitSize(frame2.blocksize, true);
                                frame3.window_buffer    = frame2.window_buffer;
                                frame3.nSeg             = frame2.nSeg;
                                frame3.current.residual = frame2.current.residual + 5 * frame2.blocksize;

                                for(var ch = 0; ch < 4; ch++)
                                {
                                    frame3.subframes[ch]
                                          .Init(frame2.subframes[ch].samples + frame2.blocksize,
                                                frame3.current.residual      + (ch + 1) * frame3.blocksize,
                                                frame.subframes[ch].obits    + frame.subframes[ch].wbits,
                                                frame.subframes[ch].wbits);
                                }

                                estimate_frame(frame3, true);
                                fs3 = measure_frame_size(frame3, true);
                            }

                            if(fs2 + fs3 > fs) break;
                            FlacFrame tmp = frame;
                            frame  = frame2;
                            frame2 = tmp;
                            fs     = fs2;

                            if(eparams.variable_block_size <= 2) break;
                            tumbler = 1 - tumbler;
                        }
                    }

                    frame.ChooseSubframes();
                    encode_estimated_frame(frame);
                }

                var bitwriter = new BitWriter(frame_buffer, 0, max_frame_size);

                output_frame_header(frame, bitwriter);
                output_subframes(frame, bitwriter);
                output_frame_footer(bitwriter);

                if(bitwriter.Length >= max_frame_size) throw new Exception("buffer overflow");

                if(frame_buffer != null)
                {
                    if(eparams.variable_block_size > 0)
                        frame_count += frame.blocksize;
                    else
                        frame_count++;
                }

                size = frame.blocksize;

                return bitwriter.Length;
            }
        }
    }

    unsafe int output_frame()
    {
        if(verify != null)
        {
            fixed(int* s = verifyBuffer, r = samplesBuffer)
            {
                for(var ch = 0; ch < channels; ch++)
                {
                    AudioSamples.MemCpy(s + ch * FlakeConstants.MAX_BLOCKSIZE,
                                        r + ch * FlakeConstants.MAX_BLOCKSIZE,
                                        m_blockSize);
                }
            }
        }

        int fs, bs;

        //if (0 != eparams.variable_block_size && 0 == (m_blockSize & 7) && m_blockSize >= 128)
        //    fs = encode_frame_vbs();
        //else
        fs = encode_frame(out bs);

        if(seek_table != null && _IO.CanSeek)
        {
            for(var sp = 0; sp < seek_table.Length; sp++)
            {
                if(seek_table[sp].framesize != 0) continue;

                if(seek_table[sp].number > Position + bs) break;

                if(seek_table[sp].number >= Position)
                {
                    seek_table[sp].number    = Position;
                    seek_table[sp].offset    = _IO.Position - first_frame_offset;
                    seek_table[sp].framesize = bs;
                }
            }
        }

        Position += bs;
        _IO.Write(frame_buffer, 0, fs);
        TotalSize += fs;

        if(verify != null)
        {
            try
            {
                int decoded = verify.DecodeFrame(frame_buffer, 0, fs);

                if(decoded != fs || verify.Remaining != bs) throw new Exception(Resources.ExceptionValidationFailed);

                fixed(int* s = verifyBuffer, r = verify.Samples)
                {
                    for(var ch = 0; ch < channels; ch++)
                    {
                        if(AudioSamples.MemCmp(s + ch * FlakeConstants.MAX_BLOCKSIZE,
                                               r + ch * FlakeConstants.MAX_BLOCKSIZE,
                                               bs))
                            throw new Exception(Resources.ExceptionValidationFailed);
                    }
                }
            }
            catch(Exception ex)
            {
                //if (channels == 2)
                //{
                //    var sw = new WAVWriter(string.Format("verify_{0}.wav", this.frame_count), new WAVWriterSettings(this.Settings.PCM));
                //    sw.FinalSampleCount = this.frame.blocksize;
                //    var ab = new AudioBuffer(this.Settings.PCM, this.frame.blocksize);
                //    ab.Prepare(this.frame.blocksize);
                //    fixed (int* abs = ab.Samples, s = verifyBuffer)
                //        AudioSamples.Interlace(abs, s, s + FlakeConstants.MAX_BLOCKSIZE, this.frame.blocksize);
                //    sw.Write(ab);
                //    sw.Close();
                //} else
                throw ex;
            }
        }

        if(bs < m_blockSize)
        {
            for(var ch = 0; ch < (channels == 2 ? 4 : channels); ch++)
            {
                Buffer.BlockCopy(samplesBuffer,
                                 (bs + ch * FlakeConstants.MAX_BLOCKSIZE) * sizeof(int),
                                 samplesBuffer,
                                 ch                 * FlakeConstants.MAX_BLOCKSIZE * sizeof(int),
                                 (m_blockSize - bs) * sizeof(int));
            }

            //fixed (int* s = samplesBuffer)
            //    for (int ch = 0; ch < channels; ch++)
            //        AudioSamples.MemCpy(s + ch * FlakeConstants.MAX_BLOCKSIZE, s + bs + ch * FlakeConstants.MAX_BLOCKSIZE, m_blockSize - bs);
        }

        samplesInBuffer -= bs;

        return bs;
    }

    public void Write(AudioBuffer buff)
    {
        if(!inited)
        {
            if(_IO == null) _IO = new FileStream(Path, FileMode.Create, FileAccess.Write, FileShare.Read, 0x20000);
            inited = true;
            int header_size = flake_encode_init();
            _IO.Write(header, 0, header_size);
            if(_IO.CanSeek) first_frame_offset = _IO.Position;
        }

        buff.Prepare(this);

        var pos = 0;

        while(pos < buff.Length)
        {
            int block = Math.Min(buff.Length - pos, m_blockSize - samplesInBuffer);

            copy_samples(buff.Samples, pos, block);

            pos += block;

            while(samplesInBuffer >= m_blockSize) output_frame();
        }

        if(md5 != null) md5.TransformBlock(buff.Bytes, 0, buff.ByteLength, null, 0);
    }

    public string Path { get; }

    public static string Vendor
    {
        get
        {
            Version version = typeof(AudioEncoder).Assembly.GetName().Version;

            return vendor_string ?? "CUETools " + version.Major + "." + version.Minor + "." + version.Build;
        }
        set => vendor_string = value;
    }

    static string vendor_string;

    int select_blocksize(int samplerate, int time_ms)
    {
        int blocksize = FlakeConstants.flac_blocksizes[1];
        int target    = samplerate * time_ms / 1000;

        if(eparams.variable_block_size > 0)
        {
            blocksize = 1024;
            while(target >= blocksize) blocksize <<= 1;

            return blocksize >> 1;
        }

        for(var i = 8; i < FlakeConstants.flac_blocksizes.Length - 1; i++)
        {
            if(target >= FlakeConstants.flac_blocksizes[i] && FlakeConstants.flac_blocksizes[i] > blocksize)
                blocksize = FlakeConstants.flac_blocksizes[i];
        }

        return blocksize;
    }

    void write_streaminfo(byte[] header, int pos, int last)
    {
        Array.Clear(header, pos, 38);
        var bitwriter = new BitWriter(header, pos, 38);

        // metadata header
        bitwriter.writebits(1,  last);
        bitwriter.writebits(7,  (int)MetadataType.StreamInfo);
        bitwriter.writebits(24, 34);

        if(eparams.variable_block_size > 0)
            bitwriter.writebits(16, 0);
        else
            bitwriter.writebits(16, m_blockSize);

        bitwriter.writebits(16, m_blockSize);
        bitwriter.writebits(24, 0);
        bitwriter.writebits(24, max_frame_size);
        bitwriter.writebits(20, Settings.PCM.SampleRate);
        bitwriter.writebits(3,  channels                   - 1);
        bitwriter.writebits(5,  Settings.PCM.BitsPerSample - 1);

        // total samples
        if(sample_count > 0)
        {
            bitwriter.writebits(4,  0);
            bitwriter.writebits(32, sample_count);
        }
        else
        {
            bitwriter.writebits(4,  0);
            bitwriter.writebits(32, 0);
        }

        bitwriter.flush();
    }

    /**
     * Write vorbis comment metadata block to byte array.
     * Just writes the vendor string for now.
     */
    int write_vorbis_comment(byte[] comment, int pos, int len, int last)
    {
        var      bitwriter = new BitWriter(comment, pos, len);
        Encoding enc       = new UTF8Encoding();
        byte[]   str       = enc.GetBytes(Vendor);

        // metadata header
        bitwriter.writebits(1, last);
        bitwriter.writebits(7, (int)MetadataType.VorbisComment);
        var tagsLen = 0;

        if(m_settings.Tags != null)
            foreach(string t in m_settings.Tags)
                tagsLen += 4 + enc.GetByteCount(t);

        bitwriter.writebits(24, 8 + str.Length + tagsLen);
        for(var i = 0; i < 4; i++) bitwriter.writebits(8, str.Length >> i * 8 & 0xff);
        bitwriter.write(str);
        int nTags = m_settings.Tags != null ? m_settings.Tags.Length : 0;
        for(var i = 0; i < 4; i++) bitwriter.writebits(8, nTags >> i * 8 & 0xff);

        if(m_settings.Tags != null)
        {
            foreach(string tag in m_settings.Tags)
            {
                str = enc.GetBytes(tag);
                for(var i = 0; i < 4; i++) bitwriter.writebits(8, str.Length >> i * 8 & 0xff);
                bitwriter.write(str);
            }
        }

        bitwriter.flush();

        return bitwriter.Length;
    }

    int write_seekpoints(byte[] header, int pos, int last)
    {
        seek_table_offset = pos + 4;

        var bitwriter = new BitWriter(header, pos, 4 + 18 * seek_table.Length);

        // metadata header
        bitwriter.writebits(1,  last);
        bitwriter.writebits(7,  (int)MetadataType.Seektable);
        bitwriter.writebits(24, 18 * seek_table.Length);

        for(var i = 0; i < seek_table.Length; i++)
        {
            bitwriter.writebits(FlakeConstants.FLAC__STREAM_METADATA_SEEKPOINT_SAMPLE_NUMBER_LEN,
                                (ulong)seek_table[i].number);

            bitwriter.writebits(FlakeConstants.FLAC__STREAM_METADATA_SEEKPOINT_STREAM_OFFSET_LEN,
                                (ulong)seek_table[i].offset);

            bitwriter.writebits(FlakeConstants.FLAC__STREAM_METADATA_SEEKPOINT_FRAME_SAMPLES_LEN,
                                seek_table[i].framesize);
        }

        bitwriter.flush();

        return 4 + 18 * seek_table.Length;
    }

    /**
     * Write padding metadata block to byte array.
     */
    int write_padding(byte[] padding, int pos, int last, int padlen)
    {
        var bitwriter = new BitWriter(padding, pos, 4);

        // metadata header
        bitwriter.writebits(1,  last);
        bitwriter.writebits(7,  (int)MetadataType.Padding);
        bitwriter.writebits(24, padlen);

        bitwriter.flush();

        return padlen + 4;
    }

    int write_headers()
    {
        var header_size = 0;
        var last        = 0;

        // stream marker
        header[0]   =  0x66;
        header[1]   =  0x4C;
        header[2]   =  0x61;
        header[3]   =  0x43;
        header_size += 4;

        // streaminfo
        write_streaminfo(header, header_size, last);
        header_size += 38;

        // seek table
        if(_IO.CanSeek && seek_table != null) header_size += write_seekpoints(header, header_size, last);

        // vorbis comments
        if(m_settings.Padding == 0) last = 1;
        header_size += write_vorbis_comment(header, header_size, header.Length - header_size, last);

        // padding
        if(m_settings.Padding > 0)
        {
            last        =  1;
            header_size += write_padding(header, header_size, last, m_settings.Padding);
        }

        return header_size;
    }

    int flake_encode_init()
    {
        int i, header_len;

        //if(flake_validate_params(s) < 0)

        ch_code = channels - 1;

        // find samplerate in table
        for(i = 1; i < 12; i++)
        {
            if(Settings.PCM.SampleRate == FlakeConstants.flac_samplerates[i])
            {
                sr_code0 = i;

                break;
            }
        }

        // if not in table, samplerate is non-standard
        if(i == 12) throw new Exception("non-standard samplerate");

        for(i = 1; i < 8; i++)
        {
            if(Settings.PCM.BitsPerSample == FlakeConstants.flac_bitdepths[i])
            {
                bps_code = i;

                break;
            }
        }

        if(i == 8) throw new Exception("non-standard bps");

        m_blockSize = m_settings.BlockSize != 0
                          ? m_settings.BlockSize
                          : select_blocksize(Settings.PCM.SampleRate, eparams.block_time_ms);

        // set maximum encoded frame size (if larger, re-encodes in verbatim mode)
        if(channels == 2)
        {
            max_frame_size =
                16 + (m_blockSize * (Settings.PCM.BitsPerSample + Settings.PCM.BitsPerSample + 1) + 7 >> 3);
        }
        else
            max_frame_size = 16 + (m_blockSize * channels * Settings.PCM.BitsPerSample + 7 >> 3);

        if(_IO.CanSeek && eparams.do_seektable && sample_count > 0)
        {
            int seek_points_distance = Settings.PCM.SampleRate * 10;
            int num_seek_points      = 1 + sample_count / seek_points_distance; // 1 seek point per 10 seconds
            if(sample_count % seek_points_distance == 0) num_seek_points--;
            seek_table = new SeekPoint[num_seek_points];

            for(var sp = 0; sp < num_seek_points; sp++)
            {
                seek_table[sp].framesize = 0;
                seek_table[sp].offset    = 0;
                seek_table[sp].number    = sp * seek_points_distance;
            }
        }

        // output header bytes
        var      tagsLen = 0;
        Encoding enc     = new UTF8Encoding();

        if(m_settings.Tags != null)
            foreach(string t in m_settings.Tags)
                tagsLen += 4 + enc.GetByteCount(t);

        header     = new byte[m_settings.Padding + 1024 + (seek_table == null ? 0 : seek_table.Length * 18) + tagsLen];
        header_len = write_headers();

        // initialize CRC & MD5
        if(_IO.CanSeek && m_settings.DoMD5) md5 = new MD5CryptoServiceProvider();

        if(m_settings.DoVerify)
        {
            verify       = new AudioDecoder(Settings.PCM);
            verifyBuffer = new int[FlakeConstants.MAX_BLOCKSIZE * channels];
        }

        frame_buffer = new byte[max_frame_size];

        return header_len;
    }
}

struct FlakeEncodeParams
{
    // prediction order selection method
    // set by user prior to calling flake_encode_init
    // if set to less than 0, it is chosen based on compression.
    // valid values are 0 to 5
    // 0 = use maximum order only
    // 1 = use estimation
    // 2 = 2-level
    // 3 = 4-level
    // 4 = 8-level
    // 5 = full search
    // 6 = log search
    public OrderMethod order_method;

    // block time in milliseconds
    // set by the user prior to calling flake_encode_init
    // used to calculate block_size based on sample rate
    // can also be changed by user before encoding a frame
    public int block_time_ms;

    // whether to use variable block sizes
    // set by user prior to calling flake_encode_init
    // 0 = fixed block size
    // 1 = variable block size
    public int variable_block_size;

    public bool do_seektable;

    public int development_mode;

    public int flake_set_defaults(EncoderSettings settings)
    {
        order_method        = OrderMethod.Akaike;
        block_time_ms       = 105;
        variable_block_size = 0;
        do_seektable        = true;
        development_mode    = -1;

        if(settings.GetEncoderModeIndex() == 11) variable_block_size = 4;

        return 0;
    }
}