use crate::cells::push_money;
use crate::error::FormsError;
use crate::map::Form8283Map;
use crate::verify::{verify_flat, FlatPlacement};
use crate::{fmt_date, overflow, pdf};
use btctax_core::tax::packet::ReturnHeader;
use btctax_core::{DonationDetails, Form8283HowAcquired, Form8283Row, Form8283Section};
use time::macros::format_description;
const SEC_A_CLUSTERS_2023: &[(f32, f32)] = &[
(58.0, 230.0),
(404.0, 576.0),
(58.0, 122.0),
(123.0, 186.0),
(188.0, 280.0),
(281.0, 352.0),
(353.0, 424.0),
(426.0, 576.0),
];
const SEC_A_CLUSTERS_2017: &[(f32, f32)] = &[
(50.0, 235.0),
(400.0, 580.0),
(50.0, 125.0),
(118.0, 190.0),
(183.0, 285.0),
(278.0, 356.0),
(364.0, 442.0),
(450.0, 580.0),
];
const SEC_B_CLUSTERS_2023: &[(f32, f32)] = &[
(59.0, 258.0),
(504.0, 576.0),
(58.0, 130.0),
(131.0, 287.0),
(288.0, 359.0),
(504.0, 576.0),
];
const SEC_B_CLUSTERS_2017: &[(f32, f32)] = &[
(46.0, 248.0),
(487.0, 565.0),
(46.0, 125.0),
(118.0, 233.0),
(227.0, 305.0),
(400.0, 478.0),
];
fn sec_clusters(year: i32, section: Form8283Section) -> &'static [(f32, f32)] {
match (year, section) {
(2017, Form8283Section::A) => SEC_A_CLUSTERS_2017,
(2017, Form8283Section::B) => SEC_B_CLUSTERS_2017,
(_, Form8283Section::A) => SEC_A_CLUSTERS_2023,
(_, Form8283Section::B) => SEC_B_CLUSTERS_2023,
}
}
fn how_str(h: Form8283HowAcquired) -> &'static str {
match h {
Form8283HowAcquired::Purchased => "Purchased",
Form8283HowAcquired::Gift => "Gift",
Form8283HowAcquired::Other => "Other",
Form8283HowAcquired::Review => "",
}
}
fn fmt_mo_yr(d: btctax_core::TaxDate) -> Result<String, FormsError> {
let fmt = format_description!("[month]/[year]");
d.format(&fmt)
.map_err(|e| FormsError::Structure(format!("mo/yr date format: {e}")))
}
pub fn fill_form_8283(
rows: &[Form8283Row],
map: &Form8283Map,
) -> Result<Option<Vec<u8>>, FormsError> {
fill_form_8283_inner(rows, map, None)
}
pub fn fill_form_8283_full(
printed: &btctax_core::tax::printed::Printed8283Rows,
header: &ReturnHeader,
map: &Form8283Map,
) -> Result<Option<Vec<u8>>, FormsError> {
fill_form_8283_inner(printed.rows(), map, Some(header))
}
fn fill_form_8283_inner(
rows: &[Form8283Row],
map: &Form8283Map,
filer: Option<&ReturnHeader>,
) -> Result<Option<Vec<u8>>, FormsError> {
if rows.is_empty() {
return Ok(None);
}
let section = rows
.iter()
.find_map(|r| r.section)
.unwrap_or(Form8283Section::A);
let cap = match section {
Form8283Section::A => map.section_a.rows.len(),
Form8283Section::B => map.section_b.rows.len(),
}
.max(1);
let mut copies: Vec<Vec<u8>> = Vec::new();
match section {
Form8283Section::A => {
let n_copies = rows.len().div_ceil(cap).max(1);
for k in 0..n_copies {
let chunk: Vec<&Form8283Row> = rows.iter().skip(k * cap).take(cap).collect();
let details = chunk.iter().find_map(|r| r.details.as_ref());
copies.push(fill_one(&chunk, section, map, details, filer)?);
}
}
Form8283Section::B => {
for group in group_section_b(rows) {
let n = group.rows.len().div_ceil(cap).max(1);
for k in 0..n {
let chunk: Vec<&Form8283Row> =
group.rows.iter().skip(k * cap).take(cap).copied().collect();
copies.push(fill_one(&chunk, section, map, group.details, filer)?);
}
}
}
}
if copies.len() == 1 {
Ok(Some(copies.into_iter().next().expect("exactly one copy")))
} else {
Ok(Some(overflow::merge_copies(&copies)?))
}
}
struct SectionBGroup<'a> {
rows: Vec<&'a Form8283Row>,
details: Option<&'a DonationDetails>,
}
#[derive(PartialEq, Eq)]
enum IdentityKey {
Detailed {
donee_name: String,
donee_ein: Option<String>,
appraiser_name: String,
appraiser_id: Option<String>,
},
DoneeLabel(String),
}
fn identity_key(details: Option<&DonationDetails>, donee: &str) -> Option<IdentityKey> {
match details {
Some(d) => Some(IdentityKey::Detailed {
donee_name: d.donee_name.clone(),
donee_ein: d.donee_ein.clone(),
appraiser_name: d.appraiser_name.clone(),
appraiser_id: d.appraiser_tin.clone().or_else(|| d.appraiser_ptin.clone()),
}),
None if !donee.is_empty() => Some(IdentityKey::DoneeLabel(donee.to_string())),
None => None,
}
}
fn group_section_b(rows: &[Form8283Row]) -> Vec<SectionBGroup<'_>> {
let mut groups: Vec<SectionBGroup<'_>> = Vec::new();
let mut keys: Vec<Option<IdentityKey>> = Vec::new();
let mut current: Option<usize> = None;
for row in rows {
if row.section.is_some() {
let key = identity_key(row.details.as_ref(), &row.donee);
let existing = key
.as_ref()
.and_then(|k| keys.iter().position(|gk| gk.as_ref() == Some(k)));
current = Some(match existing {
Some(i) => {
groups[i].rows.push(row);
i
}
None => {
groups.push(SectionBGroup {
rows: vec![row],
details: row.details.as_ref(),
});
keys.push(key);
groups.len() - 1
}
});
} else {
match current {
Some(i) => groups[i].rows.push(row),
None => {
groups.push(SectionBGroup {
rows: vec![row],
details: None,
});
keys.push(None);
current = Some(0);
}
}
}
}
groups
}
fn push_cell(
w: &mut Vec<(String, pdf::FieldValue)>,
p: &mut Vec<FlatPlacement>,
fqn: &str,
value: String,
col: usize,
ord: u32,
) {
if value.is_empty() {
return;
}
w.push((fqn.to_string(), pdf::FieldValue::Text(value)));
p.push(FlatPlacement::cell(
fqn.to_string(),
crate::cells::page_of(fqn),
col,
col as u32,
ord,
));
}
fn push_free(
w: &mut Vec<(String, pdf::FieldValue)>,
p: &mut Vec<FlatPlacement>,
fqn: &str,
value: &str,
) {
if value.is_empty() {
return;
}
w.push((fqn.to_string(), pdf::FieldValue::Text(value.to_string())));
p.push(FlatPlacement::free(
fqn.to_string(),
crate::cells::page_of(fqn),
));
}
fn fill_one(
rows: &[&Form8283Row],
section: Form8283Section,
map: &Form8283Map,
details: Option<&DonationDetails>,
filer: Option<&ReturnHeader>,
) -> Result<Vec<u8>, FormsError> {
let mut w: Vec<(String, pdf::FieldValue)> = Vec::new();
let mut p: Vec<FlatPlacement> = Vec::new();
match section {
Form8283Section::A => {
for (i, row) in rows.iter().enumerate() {
let m = &map.section_a.rows[i];
let ord = i as u32;
push_cell(&mut w, &mut p, &m.donee, row.donee.clone(), 0, ord);
push_cell(&mut w, &mut p, &m.desc, row.description.clone(), 1, ord);
push_cell(
&mut w,
&mut p,
&m.date_contrib,
fmt_date(row.date_contributed)?,
2,
ord,
);
push_cell(
&mut w,
&mut p,
&m.date_acq,
fmt_mo_yr(row.date_acquired)?,
3,
ord,
);
push_cell(
&mut w,
&mut p,
&m.how,
how_str(row.how_acquired).to_string(),
4,
ord,
);
push_money(&mut w, &mut p, &m.cost, row.cost_basis, 5, Some((5, ord)));
push_money(&mut w, &mut p, &m.fmv, row.fmv, 6, Some((6, ord)));
push_cell(&mut w, &mut p, &m.method, row.fmv_method.clone(), 7, ord);
}
}
Form8283Section::B => {
let b = &map.section_b;
w.push((
b.k_digital_assets.field.clone(),
pdf::FieldValue::Check {
on: b.k_digital_assets.on.clone(),
},
));
p.push(FlatPlacement::check(
b.k_digital_assets.field.clone(),
crate::cells::page_of(&b.k_digital_assets.field),
));
for (i, row) in rows.iter().enumerate() {
let m = &b.rows[i];
let ord = i as u32;
let desc = match (i, &b.btc_property_note) {
(0, Some(note)) => format!("{note}: {}", row.description),
_ => row.description.clone(),
};
push_cell(&mut w, &mut p, &m.desc, desc, 0, ord);
push_money(&mut w, &mut p, &m.fmv, row.fmv, 1, Some((1, ord)));
push_cell(
&mut w,
&mut p,
&m.date_acq,
fmt_mo_yr(row.date_acquired)?,
2,
ord,
);
push_cell(
&mut w,
&mut p,
&m.how,
how_str(row.how_acquired).to_string(),
3,
ord,
);
push_money(&mut w, &mut p, &m.cost, row.cost_basis, 4, Some((4, ord)));
if let Some(ded) = row.claimed_deduction {
push_money(&mut w, &mut p, &m.deduction, ded, 5, Some((5, ord)));
}
}
if let Some(details) = details {
if let Some(name_field) = &b.appraiser_name {
push_free(&mut w, &mut p, name_field, &details.appraiser_name);
}
if let Some(a) = &details.appraiser_address {
push_free(&mut w, &mut p, &b.appraiser_address, a);
}
if let Some(tin) = details
.appraiser_tin
.as_ref()
.or(details.appraiser_ptin.as_ref())
{
push_free(&mut w, &mut p, &b.appraiser_tin, tin);
}
push_free(&mut w, &mut p, &b.donee_name, &details.donee_name);
if let Some(ein) = &details.donee_ein {
push_free(&mut w, &mut p, &b.donee_ein, ein);
}
if let Some(addr) = &details.donee_address {
push_free(&mut w, &mut p, &b.donee_address, addr);
}
}
}
};
let blank_for_identity = pdf::collect_fields(&pdf::load(pdf::f8283_pdf(map.year)?)?)?;
if let Some(header) = filer {
let blank = &blank_for_identity;
let identity = map.identity.as_ref().ok_or_else(|| {
FormsError::Geometry(format!(
"the {} Form 8283 map has no [identity] block — a full return cannot file an unnamed \
Form 8283",
map.year
))
})?;
crate::cells::push_identity(
&mut w,
&mut p,
identity,
&header.name_line,
&header.taxpayer.ssn,
blank,
)?;
}
let clusters = sec_clusters(map.year, section);
let writes = w;
let placements = p;
let mut doc = pdf::load(pdf::f8283_pdf(map.year)?)?;
let index = pdf::index(&pdf::collect_fields(&doc)?);
pdf::drop_xfa_and_set_needappearances(&mut doc)?;
pdf::apply_writes(&mut doc, &index, &writes)?;
pdf::strip_nondeterminism(&mut doc);
let bytes = pdf::save(&mut doc)?;
let check = pdf::load(&bytes)?;
let fields = pdf::collect_fields(&check)?;
verify_flat(&check, &fields, &placements, clusters)?;
Ok(bytes)
}