/*
 * Copyright (c) 2010-2013, 2015, 2017 ARM Limited
 * All rights reserved
 *
 * The license below extends only to copyright in the software and shall
 * not be construed as granting a license to any other intellectual
 * property including but not limited to intellectual property relating
 * to a hardware implementation of the functionality of the software
 * licensed hereunder.  You may use the software subject to the license
 * terms below provided that you ensure that this notice is replicated
 * unmodified and in its entirety in all distributions of the software,
 * modified or unmodified, in source code or in binary form.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met: redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer;
 * redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution;
 * neither the name of the copyright holders nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authors: Chris Emmons
 *          Andreas Sandberg
 */

#include "dev/arm/hdlcd.hh"

#include "base/output.hh"
#include "base/trace.hh"
#include "base/vnc/vncinput.hh"
#include "debug/Checkpoint.hh"
#include "debug/HDLcd.hh"
#include "dev/arm/amba_device.hh"
#include "dev/arm/base_gic.hh"
#include "enums/ImageFormat.hh"
#include "mem/packet.hh"
#include "mem/packet_access.hh"
#include "params/HDLcd.hh"
#include "sim/system.hh"

using std::vector;


// initialize hdlcd registers
HDLcd::HDLcd(const HDLcdParams *p)
    : AmbaDmaDevice(p, 0xFFFF),
      // Parameters
      vnc(p->vnc),
      workaroundSwapRB(p->workaround_swap_rb),
      workaroundDmaLineCount(p->workaround_dma_line_count),
      addrRanges{RangeSize(pioAddr, pioSize)},
      enableCapture(p->enable_capture),
      pixelBufferSize(p->pixel_buffer_size),
      virtRefreshRate(p->virt_refresh_rate),

      // Registers
      version(VERSION_RESETV),
      int_rawstat(0), int_mask(0),

      fb_base(0), fb_line_length(0), fb_line_count(0), fb_line_pitch(0),
      bus_options(BUS_OPTIONS_RESETV),

      v_sync(0), v_back_porch(0), v_data(0), v_front_porch(0),
      h_sync(0), h_back_porch(0), h_data(0), h_front_porch(0),
      polarities(0),

      command(0),

      pixel_format(0),
      red_select(0), green_select(0), blue_select(0),

      virtRefreshEvent([this]{ virtRefresh(); }, name()),
      // Other
      imgFormat(p->frame_format), pic(NULL), conv(PixelConverter::rgba8888_le),
      pixelPump(*this, *p->pxl_clk, p->pixel_chunk)
{
    if (vnc)
        vnc->setFrameBuffer(&pixelPump.fb);

    imgWriter = createImgWriter(imgFormat, &pixelPump.fb);
}

HDLcd::~HDLcd()
{
}

void
HDLcd::regStats()
{
    AmbaDmaDevice::regStats();

    using namespace Stats;

    stats.underruns
        .name(name() + ".underruns")
        .desc("number of buffer underruns")
        .flags(nozero)
        ;
}

void
HDLcd::serialize(CheckpointOut &cp) const
{
    DPRINTF(Checkpoint, "Serializing ARM HDLCD\n");

    SERIALIZE_SCALAR(int_rawstat);
    SERIALIZE_SCALAR(int_mask);

    SERIALIZE_SCALAR(fb_base);
    SERIALIZE_SCALAR(fb_line_length);
    SERIALIZE_SCALAR(fb_line_count);
    SERIALIZE_SCALAR(fb_line_pitch);
    SERIALIZE_SCALAR(bus_options);

    SERIALIZE_SCALAR(v_sync);
    SERIALIZE_SCALAR(v_back_porch);
    SERIALIZE_SCALAR(v_data);
    SERIALIZE_SCALAR(v_front_porch);

    SERIALIZE_SCALAR(h_sync);
    SERIALIZE_SCALAR(h_back_porch);
    SERIALIZE_SCALAR(h_data);
    SERIALIZE_SCALAR(h_front_porch);

    SERIALIZE_SCALAR(polarities);

    SERIALIZE_SCALAR(command);
    SERIALIZE_SCALAR(pixel_format);
    SERIALIZE_SCALAR(red_select);
    SERIALIZE_SCALAR(green_select);
    SERIALIZE_SCALAR(blue_select);

    SERIALIZE_OBJ(pixelPump);
    if (enabled())
        dmaEngine->serializeSection(cp, "dmaEngine");
}

void
HDLcd::unserialize(CheckpointIn &cp)
{
    DPRINTF(Checkpoint, "Unserializing ARM HDLCD\n");

    UNSERIALIZE_SCALAR(int_rawstat);
    UNSERIALIZE_SCALAR(int_mask);

    UNSERIALIZE_SCALAR(fb_base);
    UNSERIALIZE_SCALAR(fb_line_length);
    UNSERIALIZE_SCALAR(fb_line_count);
    UNSERIALIZE_SCALAR(fb_line_pitch);
    UNSERIALIZE_SCALAR(bus_options);

    UNSERIALIZE_SCALAR(v_sync);
    UNSERIALIZE_SCALAR(v_back_porch);
    UNSERIALIZE_SCALAR(v_data);
    UNSERIALIZE_SCALAR(v_front_porch);

    UNSERIALIZE_SCALAR(h_sync);
    UNSERIALIZE_SCALAR(h_back_porch);
    UNSERIALIZE_SCALAR(h_data);
    UNSERIALIZE_SCALAR(h_front_porch);

    UNSERIALIZE_SCALAR(polarities);

    UNSERIALIZE_SCALAR(command);
    UNSERIALIZE_SCALAR(pixel_format);
    UNSERIALIZE_SCALAR(red_select);
    UNSERIALIZE_SCALAR(green_select);
    UNSERIALIZE_SCALAR(blue_select);

    {
        // Try to unserialize the pixel pump. It might not exist if
        // we're unserializing an old checkpoint.
        ScopedCheckpointSection sec(cp, "pixelPump");
        if (cp.sectionExists(Serializable::currentSection()))
            pixelPump.unserialize(cp);
    }

    if (enabled()) {
        // Create the DMA engine and read its state from the
        // checkpoint. We don't need to worry about the pixel pump as
        // it is a proper SimObject.
        createDmaEngine();
        dmaEngine->unserializeSection(cp, "dmaEngine");

        conv = pixelConverter();
    }
}

void
HDLcd::drainResume()
{
    AmbaDmaDevice::drainResume();

    if (enabled()) {
        if (sys->bypassCaches()) {
            // We restart the HDLCD if we are in KVM mode. This
            // ensures that we always use the fast refresh logic if we
            // resume in KVM mode.
            cmdDisable();
            cmdEnable();
        } else if (!pixelPump.active()) {
            // We restored from an old checkpoint without a pixel
            // pump, start an new refresh. This typically happens when
            // restoring from old checkpoints.
            cmdEnable();
        }
    }

    // We restored from a checkpoint and need to update the VNC server
    if (pixelPump.active() && vnc)
        vnc->setDirty();
}

void
HDLcd::virtRefresh()
{
    pixelPump.renderFrame();
    schedule(virtRefreshEvent, (curTick() + virtRefreshRate));
}

// read registers and frame buffer
Tick
HDLcd::read(PacketPtr pkt)
{
    assert(pkt->getAddr() >= pioAddr &&
           pkt->getAddr() < pioAddr + pioSize);

    const Addr daddr(pkt->getAddr() - pioAddr);
    panic_if(pkt->getSize() != 4,
             "Unhandled read size (address: 0x.4x, size: %u)",
             daddr, pkt->getSize());

    const uint32_t data(readReg(daddr));
    DPRINTF(HDLcd, "read register 0x%04x: 0x%x\n", daddr, data);

    pkt->set<uint32_t>(data);
    pkt->makeAtomicResponse();
    return pioDelay;
}

// write registers and frame buffer
Tick
HDLcd::write(PacketPtr pkt)
{
    assert(pkt->getAddr() >= pioAddr &&
           pkt->getAddr() < pioAddr + pioSize);

    const Addr daddr(pkt->getAddr() - pioAddr);
    panic_if(pkt->getSize() != 4,
             "Unhandled read size (address: 0x.4x, size: %u)",
             daddr, pkt->getSize());
    const uint32_t data(pkt->get<uint32_t>());
    DPRINTF(HDLcd, "write register 0x%04x: 0x%x\n", daddr, data);

    writeReg(daddr, data);

    pkt->makeAtomicResponse();
    return pioDelay;
}

uint32_t
HDLcd::readReg(Addr offset)
{
    switch (offset) {
      case Version: return version;

      case Int_RawStat: return int_rawstat;
      case Int_Clear:
        panic("HDLCD INT_CLEAR register is Write-Only\n");
      case Int_Mask: return int_mask;
      case Int_Status: return intStatus();

      case Fb_Base: return fb_base;
      case Fb_Line_Length: return fb_line_length;
      case Fb_Line_Count: return fb_line_count;
      case Fb_Line_Pitch: return fb_line_pitch;
      case Bus_Options: return bus_options;

      case V_Sync: return v_sync;
      case V_Back_Porch: return v_back_porch;
      case V_Data: return v_data;
      case V_Front_Porch: return v_front_porch;
      case H_Sync: return h_sync;
      case H_Back_Porch: return h_back_porch;
      case H_Data: return h_data;
      case H_Front_Porch: return h_front_porch;
      case Polarities: return polarities;

      case Command: return command;
      case Pixel_Format: return pixel_format;
      case Red_Select: return red_select;
      case Green_Select: return green_select;
      case Blue_Select: return blue_select;

      default:
        panic("Tried to read HDLCD register that doesn't  exist\n", offset);
    }
}

void
HDLcd::writeReg(Addr offset, uint32_t value)
{
    switch (offset) {
      case Version:
        panic("HDLCD VERSION register is read-Only\n");

      case Int_RawStat:
        intRaise(value);
        return;
      case Int_Clear:
        intClear(value);
        return;
      case Int_Mask:
        intMask(value);
        return;
      case Int_Status:
        panic("HDLCD INT_STATUS register is read-Only\n");
        break;

      case Fb_Base:
        fb_base = value;
        return;

      case Fb_Line_Length:
        fb_line_length = value;
        return;

      case Fb_Line_Count:
        fb_line_count = value;
        return;

      case Fb_Line_Pitch:
        fb_line_pitch = value;
        return;

      case Bus_Options: {
          const BusOptsReg old_bus_options(bus_options);
          bus_options = value;

          if (bus_options.max_outstanding != old_bus_options.max_outstanding) {
              DPRINTF(HDLcd,
                      "Changing HDLcd outstanding DMA transactions: %d -> %d\n",
                      old_bus_options.max_outstanding,
                      bus_options.max_outstanding);

          }

          if (bus_options.burst_len != old_bus_options.burst_len) {
              DPRINTF(HDLcd,
                      "Changing HDLcd DMA burst flags: 0x%x -> 0x%x\n",
                      old_bus_options.burst_len, bus_options.burst_len);
          }
      } return;

      case V_Sync:
        v_sync = value;
        return;
      case V_Back_Porch:
        v_back_porch = value;
        return;
      case V_Data:
        v_data = value;
        return;
      case V_Front_Porch:
        v_front_porch = value;
        return;

      case H_Sync:
        h_sync = value;
        return;
      case H_Back_Porch:
        h_back_porch = value;
        return;
      case H_Data:
        h_data = value;
        return;
      case H_Front_Porch:
        h_front_porch = value;
        return;

      case Polarities:
        polarities = value;
        return;

      case Command: {
          const CommandReg new_command(value);

          if (new_command.enable != command.enable) {
              DPRINTF(HDLcd, "HDLCD switched %s\n",
                      new_command.enable ? "on" : "off");

              if (new_command.enable) {
                  cmdEnable();
              } else {
                  cmdDisable();
              }
          }
          command = new_command;
      } return;

      case Pixel_Format:
        pixel_format = value;
        return;

      case Red_Select:
        red_select = value;
        return;
      case Green_Select:
        green_select = value;
        return;
      case Blue_Select:
        blue_select = value;
        return;

      default:
        panic("Tried to write HDLCD register that doesn't exist\n", offset);
        return;
    }
}

PixelConverter
HDLcd::pixelConverter() const
{
    ByteOrder byte_order(
        pixel_format.big_endian ? BigEndianByteOrder : LittleEndianByteOrder);

    /* Some Linux kernels have a broken driver that swaps the red and
     * blue color select registers. */
    if (!workaroundSwapRB) {
        return PixelConverter(
            pixel_format.bytes_per_pixel + 1,
            red_select.offset, green_select.offset, blue_select.offset,
            red_select.size, green_select.size, blue_select.size,
            byte_order);
    } else {
        return PixelConverter(
            pixel_format.bytes_per_pixel + 1,
            blue_select.offset, green_select.offset, red_select.offset,
            blue_select.size, green_select.size, red_select.size,
            byte_order);
    }
}

DisplayTimings
HDLcd::displayTimings() const
{
    return DisplayTimings(
        h_data.val + 1, v_data.val + 1,
        h_back_porch.val + 1, h_sync.val + 1, h_front_porch.val + 1,
        v_back_porch.val + 1, v_sync.val + 1, v_front_porch.val + 1);
}

void
HDLcd::createDmaEngine()
{
    if (bus_options.max_outstanding == 0) {
        warn("Maximum number of outstanding DMA transfers set to 0.");
        return;
    }

    const uint32_t dma_burst_flags(bus_options.burst_len);
    const uint32_t dma_burst_len(
        dma_burst_flags ?
        (1UL << (findMsbSet(dma_burst_flags) - 1)) :
        MAX_BURST_LEN);
    // Some drivers seem to set the DMA line count incorrectly. This
    // could either be a driver bug or a specification bug. Unlike for
    // timings, the specification does not require 1 to be added to
    // the DMA engine's line count.
    const uint32_t dma_lines(
        fb_line_count + (workaroundDmaLineCount ? 1 : 0));

    dmaEngine.reset(new DmaEngine(
                        *this, pixelBufferSize,
                        AXI_PORT_WIDTH * dma_burst_len,
                        bus_options.max_outstanding,
                        fb_line_length, fb_line_pitch, dma_lines));
}

void
HDLcd::cmdEnable()
{
    createDmaEngine();
    conv = pixelConverter();

    // Update timing parameter before rendering frames
    pixelPump.updateTimings(displayTimings());

    if (sys->bypassCaches()) {
        schedule(virtRefreshEvent, clockEdge());
    } else {
        pixelPump.start();
    }
}

void
HDLcd::cmdDisable()
{
    pixelPump.stop();
    // Disable the virtual refresh event
    if (virtRefreshEvent.scheduled()) {
        assert(sys->bypassCaches());
        deschedule(virtRefreshEvent);
    }
    dmaEngine->abortFrame();
}

bool
HDLcd::pxlNext(Pixel &p)
{
    uint8_t pixel_data[MAX_PIXEL_SIZE];
    assert(conv.length <= sizeof(pixel_data));
    if (dmaEngine->tryGet(pixel_data, conv.length)) {
        p = conv.toPixel(pixel_data);
        return true;
    } else {
        return false;
    }
}

void
HDLcd::pxlVSyncBegin()
{
    DPRINTF(HDLcd, "Raising VSYNC interrupt.\n");
    intRaise(INT_VSYNC);
}

void
HDLcd::pxlVSyncEnd()
{
    DPRINTF(HDLcd, "End of VSYNC, starting DMA engine\n");
    dmaEngine->startFrame(fb_base);
}

void
HDLcd::pxlUnderrun()
{
    DPRINTF(HDLcd, "Buffer underrun, stopping DMA fill.\n");
    ++stats.underruns;
    intRaise(INT_UNDERRUN);
    dmaEngine->abortFrame();
}

void
HDLcd::pxlFrameDone()
{
    DPRINTF(HDLcd, "Reached end of last visible line.\n");

    if (dmaEngine->size()) {
        warn("HDLCD %u bytes still in FIFO after frame: Ensure that DMA "
             "and PixelPump configuration is consistent\n",
             dmaEngine->size());
        dmaEngine->dumpSettings();
        pixelPump.dumpSettings();
    }

    if (vnc)
        vnc->setDirty();

    if (enableCapture) {
        if (!pic) {
            pic = simout.create(
                csprintf("%s.framebuffer.%s",
                         sys->name(), imgWriter->getImgExtension()),
                true);
        }

        assert(pic);
        pic->stream()->seekp(0);
        imgWriter->write(*pic->stream());
    }
}

void
HDLcd::setInterrupts(uint32_t ints, uint32_t mask)
{
    const bool old_ints(intStatus());

    int_mask = mask;
    int_rawstat = ints;

    if (!old_ints && intStatus()) {
        gic->sendInt(intNum);
    } else if (old_ints && !intStatus()) {
        gic->clearInt(intNum);
    }
}

HDLcd::DmaEngine::DmaEngine(HDLcd &_parent, size_t size,
          unsigned request_size, unsigned max_pending,
          size_t line_size, ssize_t line_pitch, unsigned num_lines)
    : DmaReadFifo(
        _parent.dmaPort, size, request_size, max_pending,
        Request::UNCACHEABLE),
      parent(_parent),
      lineSize(line_size), linePitch(line_pitch), numLines(num_lines),
      nextLineAddr(0)
{
}

void
HDLcd::DmaEngine::serialize(CheckpointOut &cp) const
{
    DmaReadFifo::serialize(cp);

    SERIALIZE_SCALAR(nextLineAddr);
    SERIALIZE_SCALAR(frameEnd);
}

void
HDLcd::DmaEngine::unserialize(CheckpointIn &cp)
{
    DmaReadFifo::unserialize(cp);

    UNSERIALIZE_SCALAR(nextLineAddr);
    UNSERIALIZE_SCALAR(frameEnd);
}

void
HDLcd::DmaEngine::startFrame(Addr fb_base)
{
    nextLineAddr = fb_base;
    frameEnd = fb_base + numLines * linePitch;

    startFill(nextLineAddr, lineSize);
}

void
HDLcd::DmaEngine::abortFrame()
{
    nextLineAddr = frameEnd;
    stopFill();
    flush();
}


void
HDLcd::DmaEngine::dumpSettings()
{
    inform("DMA line size: %u bytes", lineSize);
    inform("DMA line pitch: %i bytes", linePitch);
    inform("DMA num lines: %u", numLines);
}

void
HDLcd::DmaEngine::onEndOfBlock()
{
    if (nextLineAddr == frameEnd)
        // We're done with this frame. Ignore calls to this method
        // until the next frame has been started.
        return;

    nextLineAddr += linePitch;
    if (nextLineAddr != frameEnd)
        startFill(nextLineAddr, lineSize);
}

void
HDLcd::DmaEngine::onIdle()
{
    parent.intRaise(INT_DMA_END);
}

void
HDLcd::PixelPump::dumpSettings()
{
    const DisplayTimings &t(timings());

    inform("PixelPump width: %u", t.width);
    inform("PixelPump height: %u", t.height);

    inform("PixelPump horizontal back porch: %u", t.hBackPorch);
    inform("PixelPump horizontal fron porch: %u", t.hFrontPorch);
    inform("PixelPump horizontal fron porch: %u", t.hSync);

    inform("PixelPump vertical back porch: %u", t.vBackPorch);
    inform("PixelPump vertical fron porch: %u", t.vFrontPorch);
    inform("PixelPump vertical fron porch: %u", t.vSync);
}


HDLcd *
HDLcdParams::create()
{
    return new HDLcd(this);
}