
/*

  KLayout Layout Viewer
  Copyright (C) 2006-2018 Matthias Koefferlein

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

*/


#include "extGerberDrillFileReader.h"

namespace ext
{

// ---------------------------------------------------------------------------------
//  GerberDrillFileReader implementation

GerberDrillFileReader::GerberDrillFileReader ()
  : GerberFileReader ()
{
  init ();
}

GerberDrillFileReader::~GerberDrillFileReader ()
{
  // .. nothing yet ..
}

bool 
GerberDrillFileReader::does_accept ()
{
  for (int i = 0; i < 100; ++i) {
    tl::Extractor ex (get_block ().c_str ());
    if (ex.test ("M71")) {
      return true;
    } else if (ex.test ("M48")) {
      return true;
    } else if (ex.test (";")) {
      if (ex.test ("Holesize")) {
        return true;
      } else if (ex.test ("T")) {
        return true;
      }
    }
  }

  return false;
}

void
GerberDrillFileReader::init ()
{
  //  Initialize reader:
  m_relative = false;
  m_x = 0.0;
  m_y = 0.0;
  m_xoff = 0.0;
  m_yoff = 0.0;
  m_current_diameter = 0.0;
  m_current_qty = 0;
  m_qty.clear ();
  m_current_tool = -1;
  m_tools.clear ();
  m_recording = false;
  m_record_pattern = false;
  m_holes.clear ();
  m_pattern.clear ();
  m_in_header = false;
  m_end_block = 0;
  m_m02_xoffset = m_m02_yoffset = 0.0;
  m_routing = false;
  m_plunged = false;
  m_linear_interpolation = false;
  m_format_set = false;
}

GerberMetaData
GerberDrillFileReader::do_scan ()
{
  GerberMetaData data;
  data.function = GerberMetaData::Hole;
  return data;
}

void
GerberDrillFileReader::do_read ()
{
  m_buffer.clear ();
  init ();

  stream ().skip ();

  //  Actually read:
  while (! stream ().at_end ()) {
    try {
      process_line (get_block ());
    } catch (tl::Exception &ex) {
      error (ex.msg ());
    }
  }

}

void 
GerberDrillFileReader::process_line (const std::string &s)
{
  tl::Extractor ex (s.c_str ());

  char c = *ex.skip ();
  double xi = m_x, yi = m_y;

  if (ex.at_end ()) {

    //  ignore empty line
 
  } else if (ex.test ("%")) {

    //  blocks starting with a '%' are ignored and end the header
    m_in_header = false;

  } else if (ex.test (";")) {

    int t = -1;
    double d = 0.0;
    long q = 0;
    double u = 0;

    while (! ex.at_end ()) {

      if (ex.test ("Holesize")) {

        double x = 0.0;
        ex.read (x);
        ex.test ("=");
        ex.read (d);

      } else if (ex.test ("Quantity")) {

        ex.test ("=");
        ex.read (q);

      } else if (ex.test ("Tolerance")) {

        ex.test ("=");
        double x = 0.0;
        ex.test ("+");
        if (ex.try_read (x)) {
          ex.test ("/");
          ex.try_read (x);
        }

      } else if (ex.test ("size")) {

        ex.test (":");
        ex.read (d);

      } else if (ex.test ("T")) {

        int x = 0;
        if (ex.try_read (x)) {
          t = x;
        }

      } else if (ex.test ("MM")) {

        u = 1000.0;

      } else if (ex.test ("INCH")) {

        u = 25400.0;

      } else if (ex.test ("MILS")) {

        u = 25.4;

      } else if (ex.test ("FILE_FORMAT")) {

        ex.test ("=");

        int l = -1, t = -1;
        ex.read (l);
        ex.expect (":");
        ex.read (t);

        set_format (l, t);
        m_format_set = true;

      } else {

        std::string s;
        ex.read (s, "");

      }

    }

    if (u == 0.0) {
      u = unit ();
    }

    if (d > 0) {
      d *= u;
      if (t >= 0) {
        m_tools.insert (std::make_pair (t, d));
      } else if (q > 0) {
        m_qty.push_back (std::make_pair (q, d));
      } else {
        m_current_diameter = d;
      }
    }

  } else if (ex.test ("ICI")) {

    if (ex.test (",ON")) {
      m_relative = true;
    } else if (ex.test (",OFF")) {
      m_relative = false;
    }

  } else if (ex.test ("FMAT")) {

    //  TODO: what to do here?

  } else if (ex.test ("INCH")) {

    set_unit (25400.0);

    if (ex.test (",")) {
      if (ex.test ("LZ")) {
        if (m_format_set) {
          set_format (false);
        } else {
          set_format (2, -1, false); // omit trailing zeroes, fixed format for inch system
        }
      } else if (ex.test ("TZ")) {
        if (m_format_set) {
          set_format (true);
        } else {
          set_format (-1, 4, true); // omit leading zeroes, fixed format for inch system
        }
      }
    }

    ex.expect_end ();

  } else if (ex.test ("METRIC")) {

    set_unit (1000.0);

    if (ex.test (",")) {
      if (ex.test ("LZ")) {
        if (m_format_set) {
          set_format (false);
        } else {
          //  TODO: what number of digits to use?
          set_format (3, -1, false); // omit trailing zeroes
        }
      } else if (ex.test ("TZ")) {
        if (m_format_set) {
          set_format (true);
        } else {
          //  TODO: what number of digits to use?
          set_format (-1, 3, true); // omit leading zeroes
        }
      }
    }

    ex.expect_end ();

  } else if (ex.test ("M")) {

    int mcode = 0;
    ex.read (mcode);

    if (mcode == 48) {
      m_in_header = true;
    } else if (mcode == 95) {
      m_in_header = false;
    } else if (mcode == 14 || mcode == 15) {
      m_plunged = true;
    } else if (mcode == 16 || mcode == 17) {
      m_plunged = false;
    } else if (mcode == 25) {
      start_step_and_repeat ();
    } else if (mcode == 8) {
      stop_step_and_repeat ();
    } else if (mcode == 97 || mcode == 98) {

      warn ("Canned text not supported");

      while (! stream ().at_end ()) {
        std::string l;
        read_line (l);
        tl::Extractor lex (l.c_str ());
        if (lex.test (";")) {
          //  comment -> drop
        } else {
          break;
        }
      }

    } else if (mcode == 2) {

      if (! ex.at_end ()) {

        while (true) {
          if (ex.test ("X")) {
            m_m02_xoffset += read_coord (ex);
          } else if (ex.test ("Y")) {
            m_m02_yoffset += read_coord (ex);
          } else {
            break;
          }
        }

        bool swapxy = false;
        double fx = 1.0;
        double fy = 1.0;

        while (ex.test ("M")) {

          int mcode2 = 0;
          ex.read (mcode2);

          if (mcode2 == 70) {
            swapxy = true;
          } else if (mcode2 == 80) {
            if (swapxy) {
              fy = -fy;
            } else {
              fx = -fx;
            }
          } else if (mcode2 == 90) {
            if (swapxy) {
              fx = -fx;
            } else {
              fy = -fy;
            }
          }
          
        }

        repeat_block (m_m02_xoffset, m_m02_yoffset, fx, fy, swapxy);

      }

    } else if (mcode == 1) {

      end_block ();

      m_m02_xoffset = 0.0;
      m_m02_yoffset = 0.0;

    }

  } else if (ex.test ("T")) {

    int tcode = 0;
    if (ex.try_read (tcode)) {

      m_current_tool = tcode;

      c = *ex.skip ();

      if (c == 'F' || c == 'S' || c == 'C') {

        while (true) {
          double d = 0.0;
          if (ex.test ("F") || ex.test ("S")) {
            ex.read (d);
          } else if (ex.test ("C")) {
            ex.read (d);
            d *= unit ();
            m_tools.insert (std::make_pair (tcode, d));
            m_current_diameter = d;
          } else {
            break;
          }
        }

      } else if (ex.test ("size")) {

        double d = 0.0;

        ex.test (":");
        ex.read (d);
        d *= unit ();

        m_tools.insert (std::make_pair (tcode, d));
        m_current_diameter = d;

      } else {

        //  following specs 

        if (m_tools.find (tcode) == m_tools.end ()) {
          if (tcode == 0) {
            //  some file formats indicate "no tool" with this code ..
            m_current_diameter = 0.0;
          } else {
            throw tl::Exception (tl::to_string (QObject::tr ("Undefined tool code %d")), tcode);
          }
        } else {
          m_current_diameter = m_tools [tcode];
        }

      }

    }

  } else if (! m_in_header && ex.test ("P")) {

    int n = 0;
    ex.read (n);

    double dx = 0.0, dy = 0.0;

    if (ex.test ("X")) {
      dx = read_coord (ex);
    }

    if (ex.test ("Y")) {
      dy = read_coord (ex);
    }

    for (int i = 1; i <= n; ++i) {
      repeat_pattern (i * dx, i * dy);
    }

  } else if (! m_in_header && ex.test ("R")) {

    int n = 0;
    ex.read (n);

    double dx = 0.0, dy = 0.0;

    bool use_block = false;
    if (ex.test ("M")) {
      int mcode = 0;
      ex.read (mcode);
      if (mcode == 2) {
        use_block = true;
      }
    } 

    if (ex.test ("X")) {
      dx = read_coord (ex);
    }

    if (ex.test ("Y")) {
      dy = read_coord (ex);
    }

    if (use_block) {

      for (int i = 1; i < n; ++i) {
        repeat_block (i * dx, i * dy);
      }

    } else {

      for (int i = 1; i <= n; ++i) {

        next_hole ();
        produce_circle (m_x + m_xoff + i * dx, m_y + m_yoff + i * dy, m_current_diameter * 0.5);

      }

      m_x += n * dx;
      m_y += n * dy;

    }

  } else if (! m_in_header && (c == 'X' || c == 'Y' || c == 'G')) {

    double xb = 0.0, yb = 0.0;
    bool has_xb = false, has_yb = false;

    while (true) {
      if (ex.test ("X")) {
        xb = read_coord (ex);
        has_xb = true;
      } else if (ex.test ("Y")) {
        yb = read_coord (ex);
        has_yb = true;
      } else {
        break;
      }
    }

    int gcode = 0;

    if (has_xb) {
      if (m_relative) {
        m_x += xb;
      } else {
        m_x = xb;
      }
    }

    if (has_yb) {
      if (m_relative) {
        m_y += yb;
      } else {
        m_y = yb;
      }
    }

    if (ex.test ("G")) {

      ex.read (gcode);

      if (gcode == 90) {
        m_relative = false;
      } else if (gcode == 91) {
        m_relative = true;
      } else if (gcode == 93 || gcode == 92) {

        double xa = 0.0, ya = 0.0;
        bool has_xa = false, has_ya = false;

        while (true) {
          if (ex.test ("X")) {
            xa = read_coord (ex);
            has_xa = true;
          } else if (ex.test ("Y")) {
            ya = read_coord (ex);
            has_ya = true;
          } else {
            break;
          }
        }

        if (has_xa) {
          m_xoff += xa; m_x = 0; 
        }

        if (has_ya) {
          m_yoff += ya; m_y = 0;
        }

      } else if (gcode == 2 || gcode == 3) {

        //  TODO: implement
        warn ("Circular interpolation not supported currently.");

      } else if (gcode == 32 || gcode == 33) {

        //  TODO: implement
        warn ("Routed canned circles not supported currently.");

      } else if (gcode == 0) {

        m_routing = true;
        m_linear_interpolation = false;

        //  process rest of line
        process_line (std::string (ex.skip ()));
        return;

      } else if (gcode == 5) {

        m_routing = false;
        m_linear_interpolation = false;

      } else if (gcode == 1) {

        m_linear_interpolation = true;

        //  process rest of line
        process_line (std::string (ex.skip ()));
        return;

      } else if (gcode == 83 || gcode == 81 || gcode == 82) {

        std::vector<std::pair<double, double> > coords;

        while (! stream ().at_end () && coords.size () < 2) {

          std::string l;
          read_line (l);
          tl::Extractor lex (l.c_str ());
          if (lex.test (";")) {
            //  comment -> drop
          } else {

            coords.push_back (std::make_pair (m_x, m_y));

            while (true) {

              if (lex.test ("X")) {

                double d = read_coord (lex);
                if (m_relative) {
                  coords.back ().first += d;
                } else {
                  coords.back ().first = d;
                }

              } else if (lex.test ("Y")) {

                double d = read_coord (lex);
                if (m_relative) {
                  coords.back ().second += d;
                } else {
                  coords.back ().second = d;
                }

              } else {
                break;
              }

            }

          }

        }

        if (coords.size () == 2) {

          begin_pattern ();

          if (gcode == 83) {

            double xc = (coords[0].first + coords[1].first) * 0.5;
            double yc = (coords[0].second + coords[1].second) * 0.5;
            double xr = xc - coords[0].first;
            double yr = yc - coords[0].second;

            for (int i = 0; i < 8; ++i) {

              double a = i * M_PI / 4;

              next_hole ();
              produce_circle (xc + m_xoff + cos (a) * xr - sin (a) * yr, yc + m_yoff + cos (a) * yr + sin (a) * xr, m_current_diameter * 0.5);

            }

          } else {

            double xa = 0.0, ya = 0.0;
            bool has_xa = false, has_ya = false;

            while (true) {
              if (ex.test ("X")) {
                xa = read_coord (ex);
                has_xa = true;
              } else if (ex.test ("Y")) {
                ya = read_coord (ex);
                has_ya = true;
              } else {
                break;
              }
            }

            if (! has_xa || xa < 1e-6) {
              xa = 0.1 * 25400.0;
            } 
            if (! has_ya || ya < 1e-6) {
              ya = 0.3 * 25400.0;
            }

            int n = 0;
            double dx = 0.0, dy = 0.0;

            if (fabs (fabs (coords[0].first - coords[1].first) - fabs (ya)) < fabs (fabs (coords[0].second - coords[1].second) - fabs (ya))) {
              //  vertical
              dx = 0.0;
              dy = xa * (coords[1].second < coords[0].second ? -1.0 : 1.0);
              n = std::max (0, int (floor (0.5 + (coords[1].second - coords[0].second) / xa)));
            } else {
              //  horizontal
              dy = 0.0;
              dx = xa * (coords[1].first < coords[0].first ? -1.0 : 1.0);
              n = std::max (0, int (floor (0.5 + (coords[1].first - coords[0].first) / xa)));
            }

            for (int i = 0; i <= n; ++i) {

              next_hole ();
              produce_circle (coords [0].first + m_xoff, coords [0].second + m_yoff, m_current_diameter * 0.5);
              coords[0].first += dx;
              coords[0].second += dy;

              next_hole ();
              produce_circle (coords [1].first + m_xoff, coords [1].second + m_yoff, m_current_diameter * 0.5);
              coords[1].first -= dx;
              coords[1].second -= dy;

            }

          }

          end_pattern ();

        }

      } else if (gcode == 85) {

        double xa = 0.0, ya = 0.0;
        bool has_xa = false, has_ya = false;

        while (true) {
          if (ex.test ("X")) {
            xa = read_coord (ex);
            has_xa = true;
          } else if (ex.test ("Y")) {
            ya = read_coord (ex);
            has_ya = true;
          } else {
            break;
          }
        }

        double x0 = m_x, y0 = m_y;

        if (has_xa) {
          if (m_relative) {
            m_x += xa;
          } else {
            m_x = xa;
          }
        }

        if (has_ya) {
          if (m_relative) {
            m_y += ya;
          } else {
            m_y = ya;
          }
        }

        next_hole ();

        //  produce the slot
        produce_circle (x0 + m_xoff, y0 + m_yoff, m_current_diameter * 0.5, m_x + m_xoff, m_y + m_yoff);

      }

    } else if (has_xb || has_yb) {

      next_hole ();

      if (!m_routing) {
        produce_circle (m_x + m_xoff, m_y + m_yoff, m_current_diameter * 0.5);
      } else if (m_plunged) {
        if (m_linear_interpolation) {
          produce_circle (xi + m_xoff, yi + m_yoff, m_current_diameter * 0.5, m_x + m_xoff, m_y + m_yoff);
        }
      }

    }

    if (! ex.at_end ()) {
      warn (tl::sprintf (tl::to_string (QObject::tr ("Part of line ignored: %s")), ex.skip ()));
    }

  } else if (!m_in_header && c != 0) {
    warn (tl::to_string (QObject::tr ("Statement ignored")));
  }
}

void 
GerberDrillFileReader::next_hole ()
{
  if (m_current_tool < 0 && !m_qty.empty ()) {
    if (m_current_qty == 0) {
      m_current_qty = m_qty.front ().first;
      m_current_diameter = m_qty.front ().second;
      m_qty.pop_front ();
    } 
    if (m_current_qty > 0) {
      --m_current_qty;
    }
  } 
}

void 
GerberDrillFileReader::read_line (std::string &b)
{
  progress_checkpoint ();

  b.clear ();

  char c;
  while (! stream ().at_end ()) {
    c = stream ().get_char ();
    if (c == '\n' || c == '\r') {
      break;
    }
    b += c;
  }

  c = stream ().peek_char ();
  if (c == '\n' || c == '\r') {
    stream ().get_char ();
  }
}

const std::string &
GerberDrillFileReader::get_block ()
{
  read_line (m_buffer);
  return m_buffer;
}

void 
GerberDrillFileReader::begin_pattern ()
{
  m_record_pattern = true;
  m_pattern.clear ();
}

void 
GerberDrillFileReader::end_pattern ()
{
  m_record_pattern = false;
}

void
GerberDrillFileReader::repeat_pattern (double dx, double dy)
{
  if (m_record_pattern) {
    return;
  }

  for (size_t i = 0; i < m_pattern.size (); ++i) {
    produce_circle (m_pattern [i].x + dx, m_pattern [i].y + dy, m_pattern [i].r, m_pattern [i].ex + dx, m_pattern [i].ey + dy);
  }
}

void
GerberDrillFileReader::start_step_and_repeat ()
{
  m_holes.clear ();
  m_recording = true;
  m_end_block = 0;
}

void
GerberDrillFileReader::stop_step_and_repeat ()
{
  m_recording = false;

  for (size_t i = 0; i < m_holes.size (); ++i) {
    produce_circle (m_holes [i].x, m_holes [i].y, m_holes [i].r, m_holes [i].ex, m_holes [i].ey);
  }
}

void
GerberDrillFileReader::repeat_block (double dx, double dy, double fx, double fy, bool swapxy)
{
  for (size_t i = 0; i < m_end_block; ++i) {

    m_holes.push_back (m_holes [i]);

    m_holes.back ().x -= m_xoff;
    m_holes.back ().y -= m_yoff;
    m_holes.back ().ex -= m_xoff;
    m_holes.back ().ey -= m_yoff;

    m_holes.back ().x *= fx;
    m_holes.back ().y *= fy;
    m_holes.back ().ex *= fx;
    m_holes.back ().ey *= fy;
    if (swapxy) {
      std::swap (m_holes.back ().x, m_holes.back ().y);
      std::swap (m_holes.back ().ex, m_holes.back ().ey);
    }

    m_holes.back ().x += m_xoff;
    m_holes.back ().y += m_yoff;
    m_holes.back ().ex += m_xoff;
    m_holes.back ().ey += m_yoff;

    m_holes.back ().x += dx;
    m_holes.back ().y += dy;
    m_holes.back ().ex += dx;
    m_holes.back ().ey += dy;

  }
}

void
GerberDrillFileReader::end_block ()
{
  m_end_block = m_holes.size ();
}

void
GerberDrillFileReader::produce_circle (double cx, double cy, double r, double ex, double ey)
{
  if (m_record_pattern) {
    m_pattern.push_back (DrillHoleDescriptor (cx, cy, r, ex, ey));
  }

  if (m_recording) {
    m_holes.push_back (DrillHoleDescriptor (cx, cy, r, ex, ey));
  } else {
    produce_circle_raw (cx, cy, r, ex, ey);
  }
}

void
GerberDrillFileReader::produce_circle_raw (double cx, double cy, double r, double ex, double ey)
{
  double mx = cx - ex;
  double my = cy - ey;
  double m = sqrt (mx * mx + my * my);
  if (m < 1e-6) {
    mx = r;
    my = 0.0;
  } else {
    mx *= r / m;
    my *= r / m;
  }

  double nx = -my;
  double ny = mx;

  std::vector <db::DPoint> points;

  int n_circle = get_circle_points ();

  //  adjust the radius so we get a outer approximation of the circle:
  //  r *= 1.0 / cos (M_PI / double (n_circle));

  int i = 0;

  for (; i < n_circle / 2; ++i) {
    double a = M_PI * 2.0 * (double (i) / double (n_circle));
    points.push_back (db::DPoint (cx + nx * cos (a) + mx * sin (a), cy + ny * cos (a) + my * sin (a)));
  }

  for (; i < n_circle; ++i) {
    double a = M_PI * 2.0 * (double (i) / double (n_circle));
    points.push_back (db::DPoint (ex + nx * cos (a) + mx * sin (a), ey + ny * cos (a) + my * sin (a)));
  }

  db::DPolygon p;
  p.assign_hull (points.begin (), points.end ());

  produce_polygon (p, false);
}

}

